From e37dd067afeb248d1e12c69aebe1f2141959fcad Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 8 Nov 2025 04:11:23 -0800 Subject: [PATCH 01/46] Scaffolding --- .../.publicApi/PublicAPI.Shipped.txt | 1 + .../.publicApi/PublicAPI.Unshipped.txt | 8 +++ .../CHANGELOG.md | 5 ++ .../KustoInstrumentationOptions.cs | 15 +++++ ...OpenTelemetry.Instrumentation.Kusto.csproj | 24 ++++++++ .../README.md | 56 +++++++++++++++++++ .../TracerProviderBuilderExtensions.cs | 42 ++++++++++++++ .../KustoInstrumentationTests.cs | 34 +++++++++++ ...lemetry.Instrumentation.Kusto.Tests.csproj | 16 ++++++ 9 files changed, 201 insertions(+) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Shipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/README.md create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..304f3ec5ef --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +#nullable enable +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.EnableTracing.get -> bool +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.EnableTracing.set -> void +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions +static OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md new file mode 100644 index 0000000000..8e73c393cd --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +* Initial implementation of Kusto instrumentation. diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs new file mode 100644 index 0000000000..236b2cb8ba --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.Kusto; + +/// +/// Options for Kusto instrumentation. +/// +public class KustoInstrumentationOptions +{ + /// + /// Gets or sets a value indicating whether to enable tracing. + /// + public bool EnableTracing { get; set; } = true; +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj new file mode 100644 index 0000000000..231d01a9cc --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -0,0 +1,24 @@ + + + + $(NetStandardMinimumSupportedVersion) + OpenTelemetry Kusto Instrumentation. + $(PackageTags);Kusto;AzureDataExplorer + Instrumentation.Kusto- + + + + + true + + + + + + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md new file mode 100644 index 0000000000..f85ae6e0ce --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -0,0 +1,56 @@ +# Kusto Instrumentation for OpenTelemetry + +| Status | | +| ----------- | --------- | +| Stability | [Experimental](../../README.md#experimental) | + +[![NuGet version badge](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.Kusto)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) +[![NuGet download count badge](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.Kusto)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) +[![codecov.io](https://codecov.io/gh/open-telemetry/opentelemetry-dotnet-contrib/branch/main/graphs/badge.svg?flag=unittests-Instrumentation.Kusto)](https://app.codecov.io/gh/open-telemetry/opentelemetry-dotnet-contrib?flags[0]=unittests-Instrumentation.Kusto) + +This is an +[Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), +which instruments Azure Data Explorer (Kusto) client libraries +and collects telemetry about Kusto operations. + +## Steps to enable OpenTelemetry.Instrumentation.Kusto + +### Step 1: Install Package + +Add a reference to the +[`OpenTelemetry.Instrumentation.Kusto`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) +package. Also, add any other instrumentations & exporters you will need. + +```shell +dotnet add package OpenTelemetry.Instrumentation.Kusto +``` + +### Step 2: Enable Kusto Instrumentation at application startup + +Kusto instrumentation must be enabled at application startup. + +The following example demonstrates adding Kusto instrumentation to a +console application. This example also sets up the OpenTelemetry Console +exporter, which requires adding the package +[`OpenTelemetry.Exporter.Console`](https://www.nuget.org/packages/OpenTelemetry.Exporter.Console) +to the application. + +```csharp +using OpenTelemetry.Trace; + +public class Program +{ + public static void Main(string[] args) + { + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddKustoInstrumentation() + .AddConsoleExporter() + .Build(); + } +} +``` + +## References + +* [OpenTelemetry Project](https://opentelemetry.io/) +* [Azure Data Explorer (Kusto)](https://docs.microsoft.com/azure/data-explorer/) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs new file mode 100644 index 0000000000..371f52fec3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -0,0 +1,42 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.Kusto; + +/// +/// Extension methods to simplify registering of Kusto instrumentation. +/// +public static class TracerProviderBuilderExtensions +{ + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + return builder.AddKustoInstrumentation(new KustoInstrumentationOptions()); + } + + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// Kusto instrumentation options. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddKustoInstrumentation( + this TracerProviderBuilder builder, + KustoInstrumentationOptions options) + { + Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(options); + + // TODO: Implement actual instrumentation + return builder; + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs new file mode 100644 index 0000000000..857fd072ac --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class KustoInstrumentationTests +{ + [Fact] + public void AddKustoInstrumentation_DoesNotThrow() + { + var builder = Sdk.CreateTracerProviderBuilder(); + + var actual = builder.AddKustoInstrumentation(); + + Assert.Same(builder, actual); + } + + [Fact] + public void AddKustoInstrumentation_WithOptions_DoesNotThrow() + { + var builder = Sdk.CreateTracerProviderBuilder(); + var options = new KustoInstrumentationOptions + { + EnableTracing = true, + }; + + var actual = builder.AddKustoInstrumentation(options); + + Assert.Same(builder, actual); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj new file mode 100644 index 0000000000..a7e58b3299 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj @@ -0,0 +1,16 @@ + + + + $(SupportedNetTargets) + $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) + + + + + + + + + + + From 06a6cf0649e1f319a22e8a5e7fce1a1f767790c0 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 8 Nov 2025 21:18:24 -0800 Subject: [PATCH 02/46] Add initial implementation --- Directory.Packages.props | 1 + opentelemetry-dotnet-contrib.slnx | 2 + .../.publicApi/PublicAPI.Unshipped.txt | 8 +- .../Implementation/KustoListener.cs | 195 ++++++++++++++++++ .../Implementation/Polyfills.cs | 19 ++ .../Implementation/TraceRecordExtensions.cs | 24 +++ .../KustoInstrumentationOptions.cs | 7 +- ...OpenTelemetry.Instrumentation.Kusto.csproj | 5 +- .../README.md | 2 +- .../TracerProviderBuilderExtensions.cs | 10 +- src/Shared/SemanticConventions.cs | 1 + .../KustoInstrumentationTests.cs | 1 - 12 files changed, 260 insertions(+), 15 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 14909e109e..bdbd9d12cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -83,6 +83,7 @@ + diff --git a/opentelemetry-dotnet-contrib.slnx b/opentelemetry-dotnet-contrib.slnx index b43576cdd9..e82f4b94f7 100644 --- a/opentelemetry-dotnet-contrib.slnx +++ b/opentelemetry-dotnet-contrib.slnx @@ -157,6 +157,7 @@ + @@ -267,6 +268,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 304f3ec5ef..910f324a0f 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -1,8 +1,6 @@ #nullable enable OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.EnableTracing.get -> bool -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.EnableTracing.set -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions -static OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Instrumentation.Kusto.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs new file mode 100644 index 0000000000..a52e4c1f2b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs @@ -0,0 +1,195 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +// TODO: Separate out metrics and add new extensions builder and update README. +// TODO: Can I pare the dependency down to just KustoUtils? + +internal sealed class KustoListener : KustoUtils.ITraceListener +{ + private const string InstrumentationName = "Kusto.Client"; + private const string InstrumentationVersion = "1.0.0"; + private const string DbSystem = "kusto"; + + private const string ClientRequestIdTagKey = "kusto.client_request_id"; + + private static readonly ActivitySource ActivitySource = new(InstrumentationName, InstrumentationVersion); + private static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion); + + // Metrics following OpenTelemetry database semantic conventions + private static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( + SemanticConventions.AttributeDbClientOperationDuration, + unit: "s", + advice: new InstrumentAdvice() { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }, + description: "Duration of database client operations"); + + private static readonly Counter OperationCounter = Meter.CreateCounter( + "db.client.operation.count", + description: "Number of database client operations"); + + public override bool IsThreadSafe => true; + + public override void Flush() + { + } + + public override void Write(KustoUtils.TraceRecord record) + { + if (record?.Message is null) + { + return; + } + + if (record.IsRequestStart()) + { + HandleHttpRequestStart(record); + } + else if (record.IsResponseStart()) + { + HandleHttpResponseReceived(record); + } + else if (record.IsException()) + { + HandleException(record); + } + } + + private static void HandleException(KustoUtils.TraceRecord record) + { + var activity = Activity.Current; + + activity?.SetStatus(ActivityStatusCode.Error, record.Message); + } + + private static void HandleHttpRequestStart(KustoUtils.TraceRecord record) + { + var operationName = record.Activity.ActivityType; + + var activity = ActivitySource.StartActivity(operationName, ActivityKind.Client); + + if (activity?.IsAllDataRequested is true) + { + activity.SetTag(SemanticConventions.AttributeDbSystemName, DbSystem); + activity.SetTag(ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); + activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); + + var message = record.Message.AsSpan(); + + var uri = ExtractValueBetween(message, "Uri=", ","); + if (!uri.IsEmpty) + { + var uriString = uri.ToString(); + activity.SetTag(SemanticConventions.AttributeUrlFull, uriString); + activity.SetTag(SemanticConventions.AttributeServerAddress, GetServerAddress(uri)); + + string? database = null; // TODO: Add parsing for database when availble + if (!string.IsNullOrEmpty(database)) + { + activity.SetTag(SemanticConventions.AttributeDbNamespace, database); + } + } + + // TODO: Consider making text optional + // TODO: Consider adding summary + var text = ExtractValueBetween(message, "text=", Environment.NewLine); + if (!text.IsEmpty) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, text.ToString()); + } + } + } + + private static void HandleHttpResponseReceived(KustoUtils.TraceRecord record) + { + var activity = Activity.Current; + + if (activity is null) + { + return; + } + + var clientRequestId = record.Activity.ClientRequestId; + var activityClientRequestId = activity.GetTagItem(ClientRequestIdTagKey) as string; + + if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) + { + var message = record.Message.AsSpan(); + var statusCode = ExtractValueBetween(message, "StatusCode=", Environment.NewLine); + CompleteHttpActivity(activity, statusCode); + } + } + + // TODO: Revisit this + private static void CompleteHttpActivity(Activity activity, ReadOnlySpan statusCode) + { + if (!statusCode.IsEmpty) + { + var statusCodeStr = statusCode.ToString(); + activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, statusCodeStr); + + // Set error status for non-2xx responses + if (!statusCodeStr.Equals("OK", StringComparison.OrdinalIgnoreCase) && + !statusCodeStr.StartsWith('2')) + { + activity.SetStatus(ActivityStatusCode.Error); + } + } + + var duration = activity.Duration.TotalSeconds; + + // Record metrics + var tags = new TagList + { + { SemanticConventions.AttributeDbSystemName, DbSystem }, + { SemanticConventions.AttributeDbOperationName, activity.DisplayName }, + }; + + OperationDurationHistogram.Record(duration, tags); + OperationCounter.Add(1, tags); + + activity.Stop(); + } + + private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, string start, string end) + { + var startIndex = source.IndexOf(start); + if (startIndex < 0) + { + return ReadOnlySpan.Empty; + } + + startIndex += start.Length; + var remaining = source.Slice(startIndex); + + var endIndex = remaining.IndexOf(end); + if (endIndex < 0) + { + endIndex = remaining.Length; + } + + return remaining.Slice(0, endIndex); + } + + private static string GetServerAddress(ReadOnlySpan uri) + { + var schemeEnd = uri.IndexOf("://"); + if (schemeEnd < 0) + { + return string.Empty; + } + + var hostStart = schemeEnd + 3; + var remaining = uri.Slice(hostStart); + + var pathStart = remaining.IndexOf('/'); + var host = pathStart >= 0 ? remaining.Slice(0, pathStart) : remaining; + + return host.ToString(); + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs new file mode 100644 index 0000000000..88e5974ca3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class Polyfills +{ +#if NETFRAMEWORK || NETSTANDARD + public static bool StartsWith(this string target, char value) + { + if (target.Length == 0) + { + return false; + } + + return target[0] == value; + } +#endif +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs new file mode 100644 index 0000000000..a345e82022 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class TraceRecordExtensions +{ + public static bool IsRequestStart(this KustoUtils.TraceRecord record) + { + return record.Message.StartsWith("$$HTTPREQUEST[", StringComparison.Ordinal); + } + + public static bool IsResponseStart(this KustoUtils.TraceRecord record) + { + return record.Message.StartsWith("$$HTTPREQUEST_RESPONSEHEADERRECEIVED[", StringComparison.Ordinal); + } + + public static bool IsException(this KustoUtils.TraceRecord record) + { + return record.TraceSourceName == "KD.Exceptions"; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index 236b2cb8ba..5bb9480cda 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -8,8 +8,7 @@ namespace OpenTelemetry.Instrumentation.Kusto; /// public class KustoInstrumentationOptions { - /// - /// Gets or sets a value indicating whether to enable tracing. - /// - public bool EnableTracing { get; set; } = true; + // TODO: Add flag for query text tracing + + // TODO: Add flag for query parameter tracing } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index 231d01a9cc..ad245c03f6 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -1,7 +1,8 @@ - $(NetStandardMinimumSupportedVersion) + $(TargetFrameworksForLibraries) + $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) OpenTelemetry Kusto Instrumentation. $(PackageTags);Kusto;AzureDataExplorer Instrumentation.Kusto- @@ -14,11 +15,13 @@ + + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index f85ae6e0ce..7cd0e77b68 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -2,7 +2,7 @@ | Status | | | ----------- | --------- | -| Stability | [Experimental](../../README.md#experimental) | +| Stability | [Alpha](../../README.md#alpha) | [![NuGet version badge](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.Kusto)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) [![NuGet download count badge](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.Kusto)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 371f52fec3..7c30086efe 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -1,10 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using OpenTelemetry.Instrumentation.Kusto; +using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; -using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; -namespace OpenTelemetry.Instrumentation.Kusto; +namespace OpenTelemetry.Trace; /// /// Extension methods to simplify registering of Kusto instrumentation. @@ -36,7 +38,9 @@ public static TracerProviderBuilder AddKustoInstrumentation( Guard.ThrowIfNull(builder); Guard.ThrowIfNull(options); - // TODO: Implement actual instrumentation + Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); + KustoUtils.TraceSourceManager.AddTraceListener(new KustoListener(), startupDone: true); + return builder; } } diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index 235b790457..dd4e911649 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -141,6 +141,7 @@ internal static class SemanticConventions // v1.36.0 database conventions: // https://github.com/open-telemetry/semantic-conventions/tree/v1.36.0/docs/database + public const string AttributeDbClientOperationDuration = "db.client.operation.duration"; public const string AttributeDbCollectionName = "db.collection.name"; public const string AttributeDbOperationName = "db.operation.name"; public const string AttributeDbSystemName = "db.system.name"; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs index 857fd072ac..be1ecaddfd 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs @@ -24,7 +24,6 @@ public void AddKustoInstrumentation_WithOptions_DoesNotThrow() var builder = Sdk.CreateTracerProviderBuilder(); var options = new KustoInstrumentationOptions { - EnableTracing = true, }; var actual = builder.AddKustoInstrumentation(options); From 350f9ef6d4e15a78f3930f0254afd7b3d6136921 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 8 Nov 2025 23:23:16 -0800 Subject: [PATCH 03/46] Add basic tests --- Directory.Packages.props | 2 + .../Implementation/KustoListener.cs | 6 +- .../TracerProviderBuilderExtensions.cs | 2 + .../KustoInstrumentationTests.cs | 33 ------- .../KustoIntegrationTests.cs | 85 +++++++++++++++++++ .../KustoIntegrationTestsFixture.cs | 36 ++++++++ .../KustoTraceProviderBuilderTests.cs | 37 ++++++++ ...lemetry.Instrumentation.Kusto.Tests.csproj | 25 +++++- ...eryTest_query=.show databases.verified.txt | 26 ++++++ ...QueryTest_query=.show version.verified.txt | 26 ++++++ ...eryTest_query=print number=42.verified.txt | 26 ++++++ .../kusto.Dockerfile | 1 + 12 files changed, 268 insertions(+), 37 deletions(-) delete mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/kusto.Dockerfile diff --git a/Directory.Packages.props b/Directory.Packages.props index bdbd9d12cb..ddc5277d1e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -138,9 +138,11 @@ + + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs index a52e4c1f2b..7c7ffdca16 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs @@ -13,14 +13,14 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal sealed class KustoListener : KustoUtils.ITraceListener { - private const string InstrumentationName = "Kusto.Client"; + internal const string ActivitySourceName = "Kusto.Client"; private const string InstrumentationVersion = "1.0.0"; private const string DbSystem = "kusto"; private const string ClientRequestIdTagKey = "kusto.client_request_id"; - private static readonly ActivitySource ActivitySource = new(InstrumentationName, InstrumentationVersion); - private static readonly Meter Meter = new(InstrumentationName, InstrumentationVersion); + private static readonly ActivitySource ActivitySource = new(ActivitySourceName, InstrumentationVersion); + private static readonly Meter Meter = new(ActivitySourceName, InstrumentationVersion); // Metrics following OpenTelemetry database semantic conventions private static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 7c30086efe..c7cf9b4ff9 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -41,6 +41,8 @@ public static TracerProviderBuilder AddKustoInstrumentation( Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); KustoUtils.TraceSourceManager.AddTraceListener(new KustoListener(), startupDone: true); + builder.AddSource(KustoListener.ActivitySourceName); + return builder; } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs deleted file mode 100644 index be1ecaddfd..0000000000 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoInstrumentationTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpenTelemetry.Trace; -using Xunit; - -namespace OpenTelemetry.Instrumentation.Kusto.Tests; - -public class KustoInstrumentationTests -{ - [Fact] - public void AddKustoInstrumentation_DoesNotThrow() - { - var builder = Sdk.CreateTracerProviderBuilder(); - - var actual = builder.AddKustoInstrumentation(); - - Assert.Same(builder, actual); - } - - [Fact] - public void AddKustoInstrumentation_WithOptions_DoesNotThrow() - { - var builder = Sdk.CreateTracerProviderBuilder(); - var options = new KustoInstrumentationOptions - { - }; - - var actual = builder.AddKustoInstrumentation(options); - - Assert.Same(builder, actual); - } -} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs new file mode 100644 index 0000000000..33bba1825d --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -0,0 +1,85 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Text; +using Kusto.Data; +using Kusto.Data.Common; +using Kusto.Data.Net.Client; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +[Trait("CategoryName", "KustoIntegrationTests")] +public sealed class KustoIntegrationTests : IClassFixture +{ + private readonly KustoIntegrationTestsFixture fixture; + + public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) + { + this.fixture = fixture; + } + + [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] + [InlineData(".show version")] + [InlineData(".show databases")] + [InlineData("print number=42")] + public Task SuccessfulQueryTest(string query) + { + var activities = new List(); + var exportedMetrics = new List(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(activities) + .AddKustoInstrumentation() + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(exportedMetrics) + .AddMeter("Kusto.Client") + .Build(); + + var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + ClientRequestId = Convert.ToBase64String(Encoding.UTF8.GetBytes(query)), + }; + + var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + + meterProvider.ForceFlush(); + + Assert.NotEmpty(activities); + var activity = activities.FirstOrDefault(a => + a.OperationName.Contains("Query") || + a.OperationName.Contains("Management") || + a.OperationName.Contains("ExecuteQuery")); + Assert.NotNull(activity); + + var activitySnapshot = new + { + activity.DisplayName, + activity.Status, + activity.StatusDescription, + activity.Tags, + }; + + Assert.NotEmpty(exportedMetrics); + var durationMetric = exportedMetrics.FirstOrDefault(m => m.Name == "db.client.operation.duration"); + Assert.NotNull(durationMetric); + + var countMetric = exportedMetrics.FirstOrDefault(m => m.Name == "db.client.operation.count"); + Assert.NotNull(countMetric); + + return Verify(activitySnapshot) + .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) + .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) + .UseDirectory("Snapshots") + .UseParameters(query); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs new file mode 100644 index 0000000000..b033a52e5c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Testcontainers.Kusto; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public sealed class KustoIntegrationTestsFixture : IAsyncLifetime +{ + private static readonly string KustoImage = GetKustoImage(); + + public KustoContainer DatabaseContainer { get; } = CreateKusto(); + + public Task InitializeAsync() => this.DatabaseContainer.StartAsync(); + + public Task DisposeAsync() => this.DatabaseContainer.DisposeAsync().AsTask(); + + private static KustoContainer CreateKusto() + => new KustoBuilder() + .WithImage(KustoImage) + .Build(); + + private static string GetKustoImage() + { + var assembly = typeof(KustoIntegrationTestsFixture).Assembly; + + using var stream = assembly.GetManifestResourceStream("kusto.Dockerfile"); + using var reader = new StreamReader(stream!); + + var raw = reader.ReadToEnd(); + + // Exclude FROM + return raw.Substring(4).Trim(); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs new file mode 100644 index 0000000000..5ced5833e5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs @@ -0,0 +1,37 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class KustoTraceProviderBuilderTests +{ + [Fact] + public void AddKustoInstrumentation_DoesNotThrow() + { + var builder = Sdk.CreateTracerProviderBuilder(); + + var actual = builder.AddKustoInstrumentation(); + + Assert.Same(builder, actual); + } + + [Fact] + public void AddKustoInstrumentation_WithNullBuilder_ThrowsArgumentNullException() + { + TracerProviderBuilder builder = null!; + + Assert.Throws(() => builder.AddKustoInstrumentation()); + } + + [Fact] + public void AddKustoInstrumentation_WithNullOptions_ThrowsArgumentNullException() + { + var builder = Sdk.CreateTracerProviderBuilder(); + KustoInstrumentationOptions options = null!; + + Assert.Throws(() => builder.AddKustoInstrumentation(options)); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj index a7e58b3299..8c2741545a 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj @@ -1,4 +1,4 @@ - + $(SupportedNetTargets) @@ -6,11 +6,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt new file mode 100644 index 0000000000..1201735629 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt @@ -0,0 +1,26 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: .show databases + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt new file mode 100644 index 0000000000..7c4063a3c5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt @@ -0,0 +1,26 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgdmVyc2lvbg== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: .show version + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt new file mode 100644 index 0000000000..b096ee298a --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt @@ -0,0 +1,26 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: print number=42 + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/kusto.Dockerfile b/test/OpenTelemetry.Instrumentation.Kusto.Tests/kusto.Dockerfile new file mode 100644 index 0000000000..17a0633c7c --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/kusto.Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/azuredataexplorer/kustainer-linux:latest@sha256:3d4f4f331fa5d7fe99ee42a0d6afd9caf53629cbcbcb0e977a6d3ba9730381da From 8cb5db7966d50a8c801e1069eb5f0a4bf7e15876 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 9 Nov 2025 02:17:15 -0800 Subject: [PATCH 04/46] Add option for query text --- .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../Implementation/KustoListener.cs | 87 ++++++++++--------- .../KustoInstrumentationOptions.cs | 6 +- .../TracerProviderBuilderExtensions.cs | 2 +- .../KustoIntegrationTests.cs | 15 ++-- .../KustoTraceProviderBuilderTests.cs | 19 ++++ ...tabases_recordQueryText=False.verified.txt | 23 +++++ ...tabases_recordQueryText=True.verified.txt} | 0 ...version_recordQueryText=False.verified.txt | 23 +++++ ...version_recordQueryText=True.verified.txt} | 0 ...mber=42_recordQueryText=False.verified.txt | 23 +++++ ...mber=42_recordQueryText=True.verified.txt} | 0 12 files changed, 153 insertions(+), 47 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt => KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt} (100%) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt => KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt} (100%) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt => KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt} (100%) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 910f324a0f..4d44a11adb 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.get -> bool +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.set -> void OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs index 7c7ffdca16..c55f766d30 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs @@ -33,6 +33,13 @@ internal sealed class KustoListener : KustoUtils.ITraceListener "db.client.operation.count", description: "Number of database client operations"); + private readonly KustoInstrumentationOptions options; + + public KustoListener(KustoInstrumentationOptions options) + { + this.options = options; + } + public override bool IsThreadSafe => true; public override void Flush() @@ -48,7 +55,7 @@ public override void Write(KustoUtils.TraceRecord record) if (record.IsRequestStart()) { - HandleHttpRequestStart(record); + this.HandleHttpRequestStart(record); } else if (record.IsResponseStart()) { @@ -67,44 +74,6 @@ private static void HandleException(KustoUtils.TraceRecord record) activity?.SetStatus(ActivityStatusCode.Error, record.Message); } - private static void HandleHttpRequestStart(KustoUtils.TraceRecord record) - { - var operationName = record.Activity.ActivityType; - - var activity = ActivitySource.StartActivity(operationName, ActivityKind.Client); - - if (activity?.IsAllDataRequested is true) - { - activity.SetTag(SemanticConventions.AttributeDbSystemName, DbSystem); - activity.SetTag(ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); - activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); - - var message = record.Message.AsSpan(); - - var uri = ExtractValueBetween(message, "Uri=", ","); - if (!uri.IsEmpty) - { - var uriString = uri.ToString(); - activity.SetTag(SemanticConventions.AttributeUrlFull, uriString); - activity.SetTag(SemanticConventions.AttributeServerAddress, GetServerAddress(uri)); - - string? database = null; // TODO: Add parsing for database when availble - if (!string.IsNullOrEmpty(database)) - { - activity.SetTag(SemanticConventions.AttributeDbNamespace, database); - } - } - - // TODO: Consider making text optional - // TODO: Consider adding summary - var text = ExtractValueBetween(message, "text=", Environment.NewLine); - if (!text.IsEmpty) - { - activity.SetTag(SemanticConventions.AttributeDbQueryText, text.ToString()); - } - } - } - private static void HandleHttpResponseReceived(KustoUtils.TraceRecord record) { var activity = Activity.Current; @@ -192,4 +161,44 @@ private static string GetServerAddress(ReadOnlySpan uri) return host.ToString(); } + + private void HandleHttpRequestStart(KustoUtils.TraceRecord record) + { + var operationName = record.Activity.ActivityType; + + var activity = ActivitySource.StartActivity(operationName, ActivityKind.Client); + + if (activity?.IsAllDataRequested is true) + { + activity.SetTag(SemanticConventions.AttributeDbSystemName, DbSystem); + activity.SetTag(ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); + activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); + + var message = record.Message.AsSpan(); + + var uri = ExtractValueBetween(message, "Uri=", ","); + if (!uri.IsEmpty) + { + var uriString = uri.ToString(); + activity.SetTag(SemanticConventions.AttributeUrlFull, uriString); + activity.SetTag(SemanticConventions.AttributeServerAddress, GetServerAddress(uri)); + + string? database = null; // TODO: Add parsing for database when availble + if (!string.IsNullOrEmpty(database)) + { + activity.SetTag(SemanticConventions.AttributeDbNamespace, database); + } + } + + // TODO: Consider adding summary + if (this.options.RecordQueryText) + { + var text = ExtractValueBetween(message, "text=", Environment.NewLine); + if (!text.IsEmpty) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, text.ToString()); + } + } + } + } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index 5bb9480cda..fdc9989477 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -8,7 +8,11 @@ namespace OpenTelemetry.Instrumentation.Kusto; /// public class KustoInstrumentationOptions { - // TODO: Add flag for query text tracing + /// + /// Gets or sets a value indicating whether the query text should be recorded as an attribute on the activity. + /// Default is false. + /// + public bool RecordQueryText { get; set; } // TODO: Add flag for query parameter tracing } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index c7cf9b4ff9..0683576361 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -39,7 +39,7 @@ public static TracerProviderBuilder AddKustoInstrumentation( Guard.ThrowIfNull(options); Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); - KustoUtils.TraceSourceManager.AddTraceListener(new KustoListener(), startupDone: true); + KustoUtils.TraceSourceManager.AddTraceListener(new KustoListener(options), startupDone: true); builder.AddSource(KustoListener.ActivitySourceName); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 33bba1825d..3b694e402c 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -24,17 +24,20 @@ public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) } [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] - [InlineData(".show version")] - [InlineData(".show databases")] - [InlineData("print number=42")] - public Task SuccessfulQueryTest(string query) + [InlineData(".show version", true)] + [InlineData(".show databases", true)] + [InlineData("print number=42", true)] + [InlineData(".show version", false)] + [InlineData(".show databases", false)] + [InlineData("print number=42", false)] + public Task SuccessfulQueryTest(string query, bool recordQueryText) { var activities = new List(); var exportedMetrics = new List(); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInMemoryExporter(activities) - .AddKustoInstrumentation() + .AddKustoInstrumentation(new KustoInstrumentationOptions { RecordQueryText = recordQueryText }) .Build(); using var meterProvider = Sdk.CreateMeterProviderBuilder() @@ -80,6 +83,6 @@ public Task SuccessfulQueryTest(string query) .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) .UseDirectory("Snapshots") - .UseParameters(query); + .UseParameters(query, recordQueryText); } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs index 5ced5833e5..b146c7e7ab 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs @@ -34,4 +34,23 @@ public void AddKustoInstrumentation_WithNullOptions_ThrowsArgumentNullException( Assert.Throws(() => builder.AddKustoInstrumentation(options)); } + + [Fact] + public void AddKustoInstrumentation_WithOptions_DoesNotThrow() + { + var builder = Sdk.CreateTracerProviderBuilder(); + var options = new KustoInstrumentationOptions { RecordQueryText = true }; + + var actual = builder.AddKustoInstrumentation(options); + + Assert.Same(builder, actual); + } + + [Fact] + public void KustoInstrumentationOptions_DefaultRecordQueryTextIsFalse() + { + var options = new KustoInstrumentationOptions(); + + Assert.False(options.RecordQueryText); + } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt new file mode 100644 index 0000000000..629a651ca5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt @@ -0,0 +1,23 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt similarity index 100% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt new file mode 100644 index 0000000000..527570a217 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt @@ -0,0 +1,23 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgdmVyc2lvbg== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt similarity index 100% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt new file mode 100644 index 0000000000..c5e5063de0 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -0,0 +1,23 @@ +{ + DisplayName: KD.RestClient.ExecuteQuery, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt similarity index 100% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt From d1e1916c6f230c64f7bf7081db86aaa3d2058814 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Thu, 13 Nov 2025 04:22:40 -0800 Subject: [PATCH 05/46] Refactor and clean up --- .gitignore | 4 ++ .../.publicApi/PublicAPI.Unshipped.txt | 3 + .../KustoActivitySourceHelper.cs | 36 +++++++++++ .../Implementation/KustoMetricListener.cs | 61 +++++++++++++++++++ ...KustoListener.cs => KustoTraceListener.cs} | 48 ++------------- .../Implementation/ListenerHandle.cs | 38 ++++++++++++ .../KustoMeterProviderBuilderExtensions.cs | 39 ++++++++++++ ...OpenTelemetry.Instrumentation.Kusto.csproj | 3 + .../TracerProviderBuilderExtensions.cs | 33 ++++++++-- .../KustoIntegrationTests.cs | 15 ++--- .../KustoMeterProviderBuilderTests.cs | 28 +++++++++ .../KustoTraceProviderBuilderTests.cs | 5 +- 12 files changed, 255 insertions(+), 58 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs rename src/OpenTelemetry.Instrumentation.Kusto/Implementation/{KustoListener.cs => KustoTraceListener.cs} (69%) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoMeterProviderBuilderTests.cs diff --git a/.gitignore b/.gitignore index a742e5530f..2814f1c368 100644 --- a/.gitignore +++ b/.gitignore @@ -446,3 +446,7 @@ test/**/BenchmarkResults/** !test/**/BenchmarkResults/results/ # Do NOT ignore files ending with -report-github.md anywhere under BenchmarkResults !test/**/BenchmarkResults/results/*-report-github.md + +# Ignore Verify received files +*.received.* +*.received/ diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 4d44a11adb..cacc69a9de 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -3,6 +3,9 @@ OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.get -> bool OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.set -> void +OpenTelemetry.Metrics.KustoMeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.KustoMeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs new file mode 100644 index 0000000000..724778b258 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs @@ -0,0 +1,36 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Reflection; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +/// +/// Helper class to hold common properties used by Kusto instrumentation. +/// +internal static class KustoActivitySourceHelper +{ + public const string DbSystem = "kusto"; + public const string ActivitySourceName = "Kusto.Client"; + public const string MeterName = "Kusto.Client"; + public const string ClientRequestIdTagKey = "kusto.client_request_id"; + + public static readonly Assembly Assembly = typeof(KustoActivitySourceHelper).Assembly; + public static readonly string PackageVersion = Assembly.GetPackageVersion(); + public static readonly ActivitySource ActivitySource = new(ActivitySourceName, PackageVersion); + public static readonly Meter Meter = new(MeterName, PackageVersion); + + public static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( + SemanticConventions.AttributeDbClientOperationDuration, + unit: "s", + advice: new InstrumentAdvice() { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }, + description: "Duration of database client operations"); + + public static readonly Counter OperationCounter = Meter.CreateCounter( + "db.client.operation.count", + description: "Number of database client operations"); +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs new file mode 100644 index 0000000000..4782bf7b2b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal sealed class KustoMetricListener : KustoUtils.ITraceListener +{ + private readonly KustoInstrumentationOptions options; + private AsyncLocal beginTimestamp = new(); + + public KustoMetricListener(KustoInstrumentationOptions options) + { + this.options = options; + } + + public override bool IsThreadSafe => true; + + public override void Flush() + { + } + + public override void Write(KustoUtils.TraceRecord record) + { + if (record?.Message is null) + { + return; + } + + if (record.IsRequestStart()) + { + this.HandleHttpRequestStart(record); + } + else if (record.IsResponseStart()) + { + this.HandleHttpResponseReceived(record); + } + } + + private static double GetElaspedTime(long start) => Stopwatch.GetTimestamp() - start; + + private void HandleHttpResponseReceived(KustoUtils.TraceRecord record) + { + var operationName = record.Activity.ActivityType; + var duration = GetElaspedTime(this.beginTimestamp.Value); + + var tags = new TagList + { + { SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem }, + { SemanticConventions.AttributeDbOperationName, operationName }, + }; + + KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, tags); + KustoActivitySourceHelper.OperationCounter.Add(1, tags); + } + + private void HandleHttpRequestStart(KustoUtils.TraceRecord record) => this.beginTimestamp.Value = Stopwatch.GetTimestamp(); +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs similarity index 69% rename from src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs rename to src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index c55f766d30..9cd07d9468 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -2,40 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Diagnostics.Metrics; using OpenTelemetry.Trace; using KustoUtils = Kusto.Cloud.Platform.Utils; namespace OpenTelemetry.Instrumentation.Kusto.Implementation; -// TODO: Separate out metrics and add new extensions builder and update README. -// TODO: Can I pare the dependency down to just KustoUtils? - -internal sealed class KustoListener : KustoUtils.ITraceListener +internal sealed class KustoTraceListener : KustoUtils.ITraceListener { - internal const string ActivitySourceName = "Kusto.Client"; - private const string InstrumentationVersion = "1.0.0"; - private const string DbSystem = "kusto"; - - private const string ClientRequestIdTagKey = "kusto.client_request_id"; - - private static readonly ActivitySource ActivitySource = new(ActivitySourceName, InstrumentationVersion); - private static readonly Meter Meter = new(ActivitySourceName, InstrumentationVersion); - - // Metrics following OpenTelemetry database semantic conventions - private static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( - SemanticConventions.AttributeDbClientOperationDuration, - unit: "s", - advice: new InstrumentAdvice() { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }, - description: "Duration of database client operations"); - - private static readonly Counter OperationCounter = Meter.CreateCounter( - "db.client.operation.count", - description: "Number of database client operations"); - private readonly KustoInstrumentationOptions options; - public KustoListener(KustoInstrumentationOptions options) + public KustoTraceListener(KustoInstrumentationOptions options) { this.options = options; } @@ -84,7 +60,7 @@ private static void HandleHttpResponseReceived(KustoUtils.TraceRecord record) } var clientRequestId = record.Activity.ClientRequestId; - var activityClientRequestId = activity.GetTagItem(ClientRequestIdTagKey) as string; + var activityClientRequestId = activity.GetTagItem(KustoActivitySourceHelper.ClientRequestIdTagKey) as string; if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) { @@ -110,18 +86,6 @@ private static void CompleteHttpActivity(Activity activity, ReadOnlySpan s } } - var duration = activity.Duration.TotalSeconds; - - // Record metrics - var tags = new TagList - { - { SemanticConventions.AttributeDbSystemName, DbSystem }, - { SemanticConventions.AttributeDbOperationName, activity.DisplayName }, - }; - - OperationDurationHistogram.Record(duration, tags); - OperationCounter.Add(1, tags); - activity.Stop(); } @@ -166,12 +130,12 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) { var operationName = record.Activity.ActivityType; - var activity = ActivitySource.StartActivity(operationName, ActivityKind.Client); + var activity = KustoActivitySourceHelper.ActivitySource.StartActivity(operationName, ActivityKind.Client); if (activity?.IsAllDataRequested is true) { - activity.SetTag(SemanticConventions.AttributeDbSystemName, DbSystem); - activity.SetTag(ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); + activity.SetTag(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); + activity.SetTag(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); var message = record.Message.AsSpan(); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs new file mode 100644 index 0000000000..66a802d894 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs @@ -0,0 +1,38 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal class ListenerHandle : IDisposable +{ + private readonly ITraceListener listener; + private bool isDisposed; + + public ListenerHandle(ITraceListener listener) + { + this.listener = listener; + TraceSourceManager.AddTraceListener(listener, startupDone: true); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + TraceSourceManager.RemoveTraceListener(this.listener); + } + + this.isDisposed = true; + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs new file mode 100644 index 0000000000..045fa58ead --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.Kusto; +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of Kusto instrumentation. +/// +public static class KustoMeterProviderBuilderExtensions +{ + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + builder.AddInstrumentation(() => + { + // TODO: Allow this to be configured + var options = new KustoInstrumentationOptions(); + + var listener = new KustoMetricListener(options); + var handle = new ListenerHandle(listener); + + return handle; + }); + + builder.AddMeter(KustoActivitySourceHelper.MeterName); + + return builder; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index ad245c03f6..b2ee898747 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -17,10 +17,13 @@ + + + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 0683576361..ac73c3d56a 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -4,7 +4,6 @@ using OpenTelemetry.Instrumentation.Kusto; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; -using KustoUtils = Kusto.Cloud.Platform.Utils; namespace OpenTelemetry.Trace; @@ -19,10 +18,23 @@ public static class TracerProviderBuilderExtensions /// being configured. /// The instance of to chain the calls. public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) + => AddKustoInstrumentation(builder, new KustoInstrumentationOptions()); + + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// Callback action for configuring . + /// The instance of to chain the calls. + public static TracerProviderBuilder AddKustoInstrumentation( + this TracerProviderBuilder builder, + Action configureKustoInstrumentationOptions) { - Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configureKustoInstrumentationOptions); - return builder.AddKustoInstrumentation(new KustoInstrumentationOptions()); + var options = new KustoInstrumentationOptions(); + configureKustoInstrumentationOptions(options); + return AddKustoInstrumentation(builder, options); } /// @@ -38,10 +50,19 @@ public static TracerProviderBuilder AddKustoInstrumentation( Guard.ThrowIfNull(builder); Guard.ThrowIfNull(options); - Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); - KustoUtils.TraceSourceManager.AddTraceListener(new KustoListener(options), startupDone: true); + builder.AddInstrumentation(() => + { + if (options.RecordQueryText) + { + Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); + } + + var listener = new KustoTraceListener(options); + var handle = new ListenerHandle(listener); + return handle; + }); - builder.AddSource(KustoListener.ActivitySourceName); + builder.AddSource(KustoActivitySourceHelper.ActivitySourceName); return builder; } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 3b694e402c..29a5b34364 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -33,16 +33,16 @@ public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) public Task SuccessfulQueryTest(string query, bool recordQueryText) { var activities = new List(); - var exportedMetrics = new List(); + var metrics = new List(); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInMemoryExporter(activities) - .AddKustoInstrumentation(new KustoInstrumentationOptions { RecordQueryText = recordQueryText }) + .AddKustoInstrumentation(options => options.RecordQueryText = recordQueryText) .Build(); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddInMemoryExporter(exportedMetrics) - .AddMeter("Kusto.Client") + .AddInMemoryExporter(metrics) + .AddKustoInstrumentation() .Build(); var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); @@ -55,6 +55,7 @@ public Task SuccessfulQueryTest(string query, bool recordQueryText) var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + tracerProvider.ForceFlush(); meterProvider.ForceFlush(); Assert.NotEmpty(activities); @@ -72,11 +73,11 @@ public Task SuccessfulQueryTest(string query, bool recordQueryText) activity.Tags, }; - Assert.NotEmpty(exportedMetrics); - var durationMetric = exportedMetrics.FirstOrDefault(m => m.Name == "db.client.operation.duration"); + Assert.NotEmpty(metrics); + var durationMetric = metrics.FirstOrDefault(m => m.Name == "db.client.operation.duration"); Assert.NotNull(durationMetric); - var countMetric = exportedMetrics.FirstOrDefault(m => m.Name == "db.client.operation.count"); + var countMetric = metrics.FirstOrDefault(m => m.Name == "db.client.operation.count"); Assert.NotNull(countMetric); return Verify(activitySnapshot) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoMeterProviderBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoMeterProviderBuilderTests.cs new file mode 100644 index 0000000000..9a29455e57 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoMeterProviderBuilderTests.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Metrics; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class KustoMeterProviderBuilderTests +{ + [Fact] + public void AddKustoInstrumentation_DoesNotThrow() + { + var builder = Sdk.CreateMeterProviderBuilder(); + + var actual = builder.AddKustoInstrumentation(); + + Assert.Same(builder, actual); + } + + [Fact] + public void AddKustoInstrumentation_WithNullBuilder_ThrowsArgumentNullException() + { + MeterProviderBuilder builder = null!; + + Assert.Throws(() => builder.AddKustoInstrumentation()); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs index b146c7e7ab..6e30438a76 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs @@ -30,7 +30,7 @@ public void AddKustoInstrumentation_WithNullBuilder_ThrowsArgumentNullException( public void AddKustoInstrumentation_WithNullOptions_ThrowsArgumentNullException() { var builder = Sdk.CreateTracerProviderBuilder(); - KustoInstrumentationOptions options = null!; + Action options = null!; Assert.Throws(() => builder.AddKustoInstrumentation(options)); } @@ -39,9 +39,8 @@ public void AddKustoInstrumentation_WithNullOptions_ThrowsArgumentNullException( public void AddKustoInstrumentation_WithOptions_DoesNotThrow() { var builder = Sdk.CreateTracerProviderBuilder(); - var options = new KustoInstrumentationOptions { RecordQueryText = true }; - var actual = builder.AddKustoInstrumentation(options); + var actual = builder.AddKustoInstrumentation(options => options.RecordQueryText = true); Assert.Same(builder, actual); } From eed9aa29e5d803aca28c03b486c6f0f6685dbbf5 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 15 Nov 2025 12:38:05 -0800 Subject: [PATCH 06/46] Fix removal and clean up tests --- .../Implementation/KustoMetricListener.cs | 17 +++++- .../Implementation/KustoTraceListener.cs | 2 + .../Implementation/ListenerHandle.cs | 13 ++++ .../KustoIntegrationTests.cs | 59 +++++++++++-------- ...tabases_recordQueryText=False.verified.txt | 51 +++++++++++----- ...atabases_recordQueryText=True.verified.txt | 57 ++++++++++++------ ...version_recordQueryText=False.verified.txt | 51 +++++++++++----- ... version_recordQueryText=True.verified.txt | 57 ++++++++++++------ ...mber=42_recordQueryText=False.verified.txt | 51 +++++++++++----- ...umber=42_recordQueryText=True.verified.txt | 57 ++++++++++++------ 10 files changed, 290 insertions(+), 125 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs index 4782bf7b2b..eeceb89854 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs @@ -17,6 +17,8 @@ public KustoMetricListener(KustoInstrumentationOptions options) this.options = options; } + public override string Name => nameof(KustoMetricListener); + public override bool IsThreadSafe => true; public override void Flush() @@ -40,7 +42,20 @@ public override void Write(KustoUtils.TraceRecord record) } } - private static double GetElaspedTime(long start) => Stopwatch.GetTimestamp() - start; + private static double GetElaspedTime(long begin) + { +#if NET + var duration = Stopwatch.GetElapsedTime(begin); +#else + var end = Stopwatch.GetTimestamp(); + var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + var delta = end - begin; + var ticks = (long)(timestampToTicks * delta); + var duration = new TimeSpan(ticks); +#endif + + return duration.TotalSeconds; + } private void HandleHttpResponseReceived(KustoUtils.TraceRecord record) { diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 9cd07d9468..5d25db8826 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -16,6 +16,8 @@ public KustoTraceListener(KustoInstrumentationOptions options) this.options = options; } + public override string Name => nameof(KustoTraceListener); + public override bool IsThreadSafe => true; public override void Flush() diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs index 66a802d894..a78d3cc858 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs @@ -13,6 +13,12 @@ internal class ListenerHandle : IDisposable public ListenerHandle(ITraceListener listener) { this.listener = listener; + + foreach (var l in TraceSourceManager.GetAllTraceListeners()) + { + Console.WriteLine($"Existing listener: {l.Name} -- {l.GetType().FullName}"); + } + TraceSourceManager.AddTraceListener(listener, startupDone: true); } @@ -30,6 +36,13 @@ protected virtual void Dispose(bool disposing) if (disposing) { TraceSourceManager.RemoveTraceListener(this.listener); + + // TODO: Follow up as this seems like a bug + foreach (var id in TraceSourceManager.GetAllTraceSourceIDs()) + { + var ts = TraceSourceManager.TryGetTraceSource(id); + ts?.RemoveTraceListener(this.listener); + } } this.isDisposed = true; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 29a5b34364..10d696e346 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -6,6 +6,7 @@ using Kusto.Data; using Kusto.Data.Common; using Kusto.Data.Net.Client; +using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Metrics; using OpenTelemetry.Tests; using OpenTelemetry.Trace; @@ -30,7 +31,7 @@ public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) [InlineData(".show version", false)] [InlineData(".show databases", false)] [InlineData("print number=42", false)] - public Task SuccessfulQueryTest(string query, bool recordQueryText) + public async Task SuccessfulQueryTest(string query, bool recordQueryText) { var activities = new List(); var metrics = new List(); @@ -50,37 +51,45 @@ public Task SuccessfulQueryTest(string query, bool recordQueryText) var crp = new ClientRequestProperties() { + // Ensure a stable client ID for snapshots ClientRequestId = Convert.ToBase64String(Encoding.UTF8.GetBytes(query)), }; - var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); tracerProvider.ForceFlush(); meterProvider.ForceFlush(); - Assert.NotEmpty(activities); - var activity = activities.FirstOrDefault(a => - a.OperationName.Contains("Query") || - a.OperationName.Contains("Management") || - a.OperationName.Contains("ExecuteQuery")); - Assert.NotNull(activity); - - var activitySnapshot = new - { - activity.DisplayName, - activity.Status, - activity.StatusDescription, - activity.Tags, - }; - - Assert.NotEmpty(metrics); - var durationMetric = metrics.FirstOrDefault(m => m.Name == "db.client.operation.duration"); - Assert.NotNull(durationMetric); - - var countMetric = metrics.FirstOrDefault(m => m.Name == "db.client.operation.count"); - Assert.NotNull(countMetric); - - return Verify(activitySnapshot) + var activitySnapshots = activities + .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) + .Select(activity => new + { + activity.DisplayName, + activity.Source.Name, + activity.Status, + activity.StatusDescription, + activity.Tags, + activity.OperationName, + activity.IdFormat, + }); + + var metricSnapshots = metrics + .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) + .Select(metric => new + { + metric.Name, + metric.Description, + metric.MeterTags, + metric.Unit, + metric.Temporality, + }); + + await Verify( + new + { + Activities = activitySnapshots, + Metrics = metricSnapshots, + }) .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) .UseDirectory("Snapshots") diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt index 629a651ca5..eb095c46ae 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt @@ -1,23 +1,44 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - server.address: {Hostname}:{Port} + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt index 1201735629..1e9e229373 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt @@ -1,26 +1,47 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: .show databases + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - db.query.text: .show databases + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt index 527570a217..d9202c3266 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt @@ -1,23 +1,44 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgdmVyc2lvbg== - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgdmVyc2lvbg== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - server.address: {Hostname}:{Port} + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt index 7c4063a3c5..8daa290181 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt @@ -1,26 +1,47 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgdmVyc2lvbg== - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: LnNob3cgdmVyc2lvbg== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: .show version + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - db.query.text: .show version + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt index c5e5063de0..45aafa21d3 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -1,23 +1,44 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - server.address: {Hostname}:{Port} + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt index b096ee298a..e0361f8ad1 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt @@ -1,26 +1,47 @@ { - DisplayName: KD.RestClient.ExecuteQuery, - Tags: [ + Activities: [ { - db.system.name: kusto - }, - { - kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - }, + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + }, + { + db.query.text: print number=42 + }, + { + http.response.status_code: OK + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ { - db.query.text: print number=42 + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative }, { - http.response.status_code: OK + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative } ] } \ No newline at end of file From d533315c43c0fe1c93ba94b3d81b386c125542b9 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 15 Nov 2025 14:07:16 -0800 Subject: [PATCH 07/46] Clean up README --- .../README.md | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index 7cd0e77b68..9d9a82bbfd 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -29,10 +29,12 @@ dotnet add package OpenTelemetry.Instrumentation.Kusto Kusto instrumentation must be enabled at application startup. -The following example demonstrates adding Kusto instrumentation to a -console application. This example also sets up the OpenTelemetry Console +#### Traces + +The following example demonstrates adding Kusto traces instrumentation +to a console application. This example also sets up the OpenTelemetry Console exporter, which requires adding the package -[`OpenTelemetry.Exporter.Console`](https://www.nuget.org/packages/OpenTelemetry.Exporter.Console) +[`OpenTelemetry.Exporter.Console`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Console/README.md) to the application. ```csharp @@ -50,7 +52,32 @@ public class Program } ``` +#### Metrics + +The following example demonstrates adding Kusto metrics instrumentation +to a console application. This example also sets up the OpenTelemetry Console +exporter, which requires adding the package +[`OpenTelemetry.Exporter.Console`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Console/README.md) +to the application. + +```csharp +using OpenTelemetry.Metrics; + +public class Program +{ + public static void Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddKustoInstrumentation() + .AddConsoleExporter() + .Build(); + } +} +``` + ## References * [OpenTelemetry Project](https://opentelemetry.io/) * [Azure Data Explorer (Kusto)](https://docs.microsoft.com/azure/data-explorer/) +* [OpenTelemetry semantic conventions for database + calls](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md) From 577510f64816f577043d5c9999aac39f2009a547 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sat, 15 Nov 2025 15:25:55 -0800 Subject: [PATCH 08/46] Add tests for exceptions --- .../Implementation/KustoMetricListener.cs | 6 +- .../Implementation/KustoTraceListener.cs | 97 ++++++++++--------- .../Implementation/TraceRecordExtensions.cs | 8 +- ...OpenTelemetry.Instrumentation.Kusto.csproj | 1 - .../KustoIntegrationTests.cs | 93 ++++++++++++++++++ ...take 10_recordQueryText=False.verified.txt | 47 +++++++++ ... take 10_recordQueryText=True.verified.txt | 47 +++++++++ ...tabases_recordQueryText=False.verified.txt | 3 - ...atabases_recordQueryText=True.verified.txt | 3 - ...version_recordQueryText=False.verified.txt | 3 - ... version_recordQueryText=True.verified.txt | 3 - ...mber=42_recordQueryText=False.verified.txt | 3 - ...umber=42_recordQueryText=True.verified.txt | 3 - 13 files changed, 245 insertions(+), 72 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs index eeceb89854..95011ce50c 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs @@ -36,9 +36,9 @@ public override void Write(KustoUtils.TraceRecord record) { this.HandleHttpRequestStart(record); } - else if (record.IsResponseStart()) + else if (record.IsActivityComplete()) { - this.HandleHttpResponseReceived(record); + this.HangleActivityComplete(record); } } @@ -57,7 +57,7 @@ private static double GetElaspedTime(long begin) return duration.TotalSeconds; } - private void HandleHttpResponseReceived(KustoUtils.TraceRecord record) + private void HangleActivityComplete(KustoUtils.TraceRecord record) { var operationName = record.Activity.ActivityType; var duration = GetElaspedTime(this.beginTimestamp.Value); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 5d25db8826..f152a9d091 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Concurrent; using System.Diagnostics; using OpenTelemetry.Trace; using KustoUtils = Kusto.Cloud.Platform.Utils; @@ -10,6 +11,7 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal sealed class KustoTraceListener : KustoUtils.ITraceListener { private readonly KustoInstrumentationOptions options; + private readonly ConcurrentDictionary activities = new(); public KustoTraceListener(KustoInstrumentationOptions options) { @@ -35,60 +37,21 @@ public override void Write(KustoUtils.TraceRecord record) { this.HandleHttpRequestStart(record); } - else if (record.IsResponseStart()) + else if (record.IsActivityComplete()) { - HandleHttpResponseReceived(record); + this.HandleActivityComplete(record); } else if (record.IsException()) { - HandleException(record); + this.HandleException(record); } } - private static void HandleException(KustoUtils.TraceRecord record) + private void HandleException(KustoUtils.TraceRecord record) { - var activity = Activity.Current; - - activity?.SetStatus(ActivityStatusCode.Error, record.Message); - } - - private static void HandleHttpResponseReceived(KustoUtils.TraceRecord record) - { - var activity = Activity.Current; - - if (activity is null) - { - return; - } - - var clientRequestId = record.Activity.ClientRequestId; - var activityClientRequestId = activity.GetTagItem(KustoActivitySourceHelper.ClientRequestIdTagKey) as string; - - if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) - { - var message = record.Message.AsSpan(); - var statusCode = ExtractValueBetween(message, "StatusCode=", Environment.NewLine); - CompleteHttpActivity(activity, statusCode); - } - } - - // TODO: Revisit this - private static void CompleteHttpActivity(Activity activity, ReadOnlySpan statusCode) - { - if (!statusCode.IsEmpty) - { - var statusCodeStr = statusCode.ToString(); - activity.SetTag(SemanticConventions.AttributeHttpResponseStatusCode, statusCodeStr); - - // Set error status for non-2xx responses - if (!statusCodeStr.Equals("OK", StringComparison.OrdinalIgnoreCase) && - !statusCodeStr.StartsWith('2')) - { - activity.SetStatus(ActivityStatusCode.Error); - } - } - - activity.Stop(); + var activity = this.GetActivity(record); + var message = ExtractValueBetween(record.Message.AsSpan(), "ErrorMessage=", Environment.NewLine); + activity?.SetStatus(ActivityStatusCode.Error, message.ToString()); } private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, string start, string end) @@ -133,6 +96,10 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) var operationName = record.Activity.ActivityType; var activity = KustoActivitySourceHelper.ActivitySource.StartActivity(operationName, ActivityKind.Client); + if (activity is not null) + { + this.activities[record.Activity.ActivityId] = activity; + } if (activity?.IsAllDataRequested is true) { @@ -167,4 +134,42 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) } } } + + private void HandleActivityComplete(KustoUtils.TraceRecord record) + { + var activity = this.GetActivity(record); + if (activity is null) + { + return; + } + + var clientRequestId = record.Activity.ClientRequestId; + var activityClientRequestId = activity.GetTagItem(KustoActivitySourceHelper.ClientRequestIdTagKey) as string; + + if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) + { + activity.Stop(); + } + +#if NET + this.activities.Remove(record.Activity.ActivityId, out _); +#else + ((IDictionary)this.activities).Remove(record.Activity.ActivityId); +#endif + } + + private Activity? GetActivity(KustoUtils.TraceRecord record) + { + if (Activity.Current is not null) + { + return Activity.Current; + } + + if (this.activities.TryGetValue(record.Activity.ActivityId, out var activity)) + { + return activity; + } + + return null; + } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs index a345e82022..ab7dbbdfa2 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordExtensions.cs @@ -12,13 +12,13 @@ public static bool IsRequestStart(this KustoUtils.TraceRecord record) return record.Message.StartsWith("$$HTTPREQUEST[", StringComparison.Ordinal); } - public static bool IsResponseStart(this KustoUtils.TraceRecord record) + public static bool IsException(this KustoUtils.TraceRecord record) { - return record.Message.StartsWith("$$HTTPREQUEST_RESPONSEHEADERRECEIVED[", StringComparison.Ordinal); + return record.TraceSourceName == "KD.Exceptions"; } - public static bool IsException(this KustoUtils.TraceRecord record) + public static bool IsActivityComplete(this KustoUtils.TraceRecord record) { - return record.TraceSourceName == "KD.Exceptions"; + return record.Message.StartsWith("MonitoredActivityCompleted", StringComparison.Ordinal); } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index b2ee898747..9f3a73d7fc 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -23,7 +23,6 @@ - diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 10d696e346..d81abe9a6b 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -57,6 +57,94 @@ public async Task SuccessfulQueryTest(string query, bool recordQueryText) using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + while (reader.Read()) + { + } + + Debugger.Break(); + + tracerProvider.ForceFlush(); + meterProvider.ForceFlush(); + + var activitySnapshots = activities + .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) + .Select(activity => new + { + activity.DisplayName, + activity.Source.Name, + activity.Status, + activity.StatusDescription, + activity.Tags, + activity.OperationName, + activity.IdFormat, + }); + + var metricSnapshots = metrics + .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) + .Select(metric => new + { + metric.Name, + metric.Description, + metric.MeterTags, + metric.Unit, + metric.Temporality, + }); + + await Verify( + new + { + Activities = activitySnapshots, + Metrics = metricSnapshots, + }) + .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) + .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) + .UseDirectory("Snapshots") + .UseParameters(query, recordQueryText); + } + + [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] + [InlineData("InvalidTable | take 10", true)] + [InlineData("InvalidTable | take 10", false)] + public async Task FailedQueryTest(string query, bool recordQueryText) + { + var activities = new List(); + var metrics = new List(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(activities) + .AddKustoInstrumentation(options => options.RecordQueryText = recordQueryText) + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(metrics) + .AddKustoInstrumentation() + .Build(); + + var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + // Ensure a stable client ID for snapshots + ClientRequestId = Convert.ToBase64String(Encoding.UTF8.GetBytes(query)), + }; + + // Execute the query and expect an exception + var exception = await Assert.ThrowsAnyAsync(async () => + { + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + + while (reader.Read()) + { + } + + Debugger.Break(); + + await Task.CompletedTask; + }); + + Debugger.Break(); + tracerProvider.ForceFlush(); meterProvider.ForceFlush(); @@ -89,6 +177,11 @@ await Verify( { Activities = activitySnapshots, Metrics = metricSnapshots, + Exception = new + { + Type = exception.GetType().Name, + HasMessage = !string.IsNullOrEmpty(exception.Message), + }, }) .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt new file mode 100644 index 0000000000..e62ca6fadb --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt @@ -0,0 +1,47 @@ +{ + Activities: [ + { + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Status: Error, + StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ + { + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative + }, + { + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative + } + ], + Exception: { + Type: SemanticException, + HasMessage: true + } +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt new file mode 100644 index 0000000000..e62ca6fadb --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt @@ -0,0 +1,47 @@ +{ + Activities: [ + { + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Status: Error, + StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', + Tags: [ + { + db.system.name: kusto + }, + { + kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + url.full: http://{Hostname}:{Port}/v1/rest/query + }, + { + server.address: {Hostname}:{Port} + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ], + Metrics: [ + { + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative + }, + { + Name: db.client.operation.count, + Description: Number of database client operations, + Unit: , + Temporality: Cumulative + } + ], + Exception: { + Type: SemanticException, + HasMessage: true + } +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt index eb095c46ae..fd5e326c8e 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt @@ -18,9 +18,6 @@ }, { server.address: {Hostname}:{Port} - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt index 1e9e229373..a1f785dd4d 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt @@ -21,9 +21,6 @@ }, { db.query.text: .show databases - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt index d9202c3266..c109b85177 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt @@ -18,9 +18,6 @@ }, { server.address: {Hostname}:{Port} - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt index 8daa290181..e68fa42ed9 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt @@ -21,9 +21,6 @@ }, { db.query.text: .show version - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt index 45aafa21d3..f6b20e2867 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -18,9 +18,6 @@ }, { server.address: {Hostname}:{Port} - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt index e0361f8ad1..3ef01504b3 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt @@ -21,9 +21,6 @@ }, { db.query.text: print number=42 - }, - { - http.response.status_code: OK } ], OperationName: KD.RestClient.ExecuteQuery, From 8738c18dcf506691e6d6d38a7a1de313dd2d33a4 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Tue, 18 Nov 2025 16:31:27 -0800 Subject: [PATCH 09/46] Refactor to use a single registration --- .../.publicApi/PublicAPI.Unshipped.txt | 5 +- .../InstrumentationHandleManagerExtensions.cs | 22 ++++++ .../Implementation/KustoInstrumentation.cs | 45 +++++++++++ .../Implementation/KustoMetricListener.cs | 9 ++- .../Implementation/KustoTraceListener.cs | 41 ++++++---- .../Implementation/ListenerHandle.cs | 51 ------------- .../KustoMeterProviderBuilderExtensions.cs | 39 ---------- .../MeterProviderBuilderExtensions.cs | 75 +++++++++++++++++++ ...OpenTelemetry.Instrumentation.Kusto.csproj | 4 +- .../TracerProviderBuilderExtensions.cs | 41 +++++----- ... take 10_recordQueryText=True.verified.txt | 3 + ...tabases_recordQueryText=False.verified.txt | 1 + ...atabases_recordQueryText=True.verified.txt | 1 + ...version_recordQueryText=False.verified.txt | 1 + ... version_recordQueryText=True.verified.txt | 1 + ...mber=42_recordQueryText=False.verified.txt | 1 + ...umber=42_recordQueryText=True.verified.txt | 1 + 17 files changed, 212 insertions(+), 129 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index cacc69a9de..edf3f8a737 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -3,9 +3,10 @@ OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.get -> bool OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.set -> void -OpenTelemetry.Metrics.KustoMeterProviderBuilderExtensions +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions -static OpenTelemetry.Metrics.KustoMeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs new file mode 100644 index 0000000000..9f8c735de3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Diagnostics; +using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class InstrumentationHandleManagerExtensions +{ + public static bool IsTracingActive(this InstrumentationHandleManager handleManager) + { + return handleManager.TracingHandles > 0; + } + + public static bool IsMetricsActive(this InstrumentationHandleManager handleManager) + { + return handleManager.MetricHandles > 0; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs new file mode 100644 index 0000000000..7f02d80fd9 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class KustoInstrumentation +{ + private static readonly Lazy TraceListener = new(() => + { + Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); + + var listener = new KustoTraceListener(); + TraceSourceManager.AddTraceListener(listener, startupDone: true); + + return listener; + }); + + private static readonly Lazy MetricListener = new(() => + { + Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); + + var listener = new KustoMetricListener(); + TraceSourceManager.AddTraceListener(listener, startupDone: true); + + return listener; + }); + + public static KustoInstrumentationOptions TracingOptions { get; set; } = new KustoInstrumentationOptions(); + + public static KustoInstrumentationOptions MetricOptions { get; set; } = new KustoInstrumentationOptions(); + + public static InstrumentationHandleManager HandleManager { get; } = new InstrumentationHandleManager(); + + public static void InitializeTracing() + { + _ = TraceListener.Value; + } + + public static void InitializeMetrics() + { + _ = MetricListener.Value; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs index 95011ce50c..acaa8a78d9 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs @@ -9,12 +9,10 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal sealed class KustoMetricListener : KustoUtils.ITraceListener { - private readonly KustoInstrumentationOptions options; private AsyncLocal beginTimestamp = new(); - public KustoMetricListener(KustoInstrumentationOptions options) + public KustoMetricListener() { - this.options = options; } public override string Name => nameof(KustoMetricListener); @@ -32,6 +30,11 @@ public override void Write(KustoUtils.TraceRecord record) return; } + if (!KustoInstrumentation.HandleManager.IsMetricsActive()) + { + return; + } + if (record.IsRequestStart()) { this.HandleHttpRequestStart(record); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index f152a9d091..32ec7aa04f 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -10,12 +10,10 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal sealed class KustoTraceListener : KustoUtils.ITraceListener { - private readonly KustoInstrumentationOptions options; private readonly ConcurrentDictionary activities = new(); - public KustoTraceListener(KustoInstrumentationOptions options) + public KustoTraceListener() { - this.options = options; } public override string Name => nameof(KustoTraceListener); @@ -33,6 +31,11 @@ public override void Write(KustoUtils.TraceRecord record) return; } + if (!KustoInstrumentation.HandleManager.IsTracingActive()) + { + return; + } + if (record.IsRequestStart()) { this.HandleHttpRequestStart(record); @@ -47,14 +50,7 @@ public override void Write(KustoUtils.TraceRecord record) } } - private void HandleException(KustoUtils.TraceRecord record) - { - var activity = this.GetActivity(record); - var message = ExtractValueBetween(record.Message.AsSpan(), "ErrorMessage=", Environment.NewLine); - activity?.SetStatus(ActivityStatusCode.Error, message.ToString()); - } - - private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, string start, string end) + private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, ReadOnlySpan start, ReadOnlySpan end) { var startIndex = source.IndexOf(start); if (startIndex < 0) @@ -76,7 +72,7 @@ private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, private static string GetServerAddress(ReadOnlySpan uri) { - var schemeEnd = uri.IndexOf("://"); + var schemeEnd = uri.IndexOf("://".AsSpan()); if (schemeEnd < 0) { return string.Empty; @@ -91,6 +87,13 @@ private static string GetServerAddress(ReadOnlySpan uri) return host.ToString(); } + private void HandleException(KustoUtils.TraceRecord record) + { + var activity = this.GetActivity(record); + var message = ExtractValueBetween(record.Message.AsSpan(), "ErrorMessage=".AsSpan(), Environment.NewLine.AsSpan()); + activity?.SetStatus(ActivityStatusCode.Error, message.ToString()); + } + private void HandleHttpRequestStart(KustoUtils.TraceRecord record) { var operationName = record.Activity.ActivityType; @@ -109,14 +112,14 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) var message = record.Message.AsSpan(); - var uri = ExtractValueBetween(message, "Uri=", ","); + var uri = ExtractValueBetween(message, "Uri=".AsSpan(), ",".AsSpan()); if (!uri.IsEmpty) { var uriString = uri.ToString(); activity.SetTag(SemanticConventions.AttributeUrlFull, uriString); activity.SetTag(SemanticConventions.AttributeServerAddress, GetServerAddress(uri)); - string? database = null; // TODO: Add parsing for database when availble + string? database = null; // TODO: Add parsing for database when available if (!string.IsNullOrEmpty(database)) { activity.SetTag(SemanticConventions.AttributeDbNamespace, database); @@ -124,9 +127,9 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) } // TODO: Consider adding summary - if (this.options.RecordQueryText) + if (KustoInstrumentation.TracingOptions.RecordQueryText) { - var text = ExtractValueBetween(message, "text=", Environment.NewLine); + var text = ExtractValueBetween(message, "text=".AsSpan(), Environment.NewLine.AsSpan()); if (!text.IsEmpty) { activity.SetTag(SemanticConventions.AttributeDbQueryText, text.ToString()); @@ -148,6 +151,12 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) { + var howEnded = ExtractValueBetween(record.Message.AsSpan(), "HowEnded=".AsSpan(), ",".AsSpan()); + if (howEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) + { + activity.SetStatus(ActivityStatusCode.Ok); + } + activity.Stop(); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs deleted file mode 100644 index a78d3cc858..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ListenerHandle.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Kusto.Cloud.Platform.Utils; - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -internal class ListenerHandle : IDisposable -{ - private readonly ITraceListener listener; - private bool isDisposed; - - public ListenerHandle(ITraceListener listener) - { - this.listener = listener; - - foreach (var l in TraceSourceManager.GetAllTraceListeners()) - { - Console.WriteLine($"Existing listener: {l.Name} -- {l.GetType().FullName}"); - } - - TraceSourceManager.AddTraceListener(listener, startupDone: true); - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - this.Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - TraceSourceManager.RemoveTraceListener(this.listener); - - // TODO: Follow up as this seems like a bug - foreach (var id in TraceSourceManager.GetAllTraceSourceIDs()) - { - var ts = TraceSourceManager.TryGetTraceSource(id); - ts?.RemoveTraceListener(this.listener); - } - } - - this.isDisposed = true; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs deleted file mode 100644 index 045fa58ead..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterProviderBuilderExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpenTelemetry.Instrumentation.Kusto; -using OpenTelemetry.Instrumentation.Kusto.Implementation; -using OpenTelemetry.Internal; - -namespace OpenTelemetry.Metrics; - -/// -/// Extension methods to simplify registering of Kusto instrumentation. -/// -public static class KustoMeterProviderBuilderExtensions -{ - /// - /// Enables Kusto instrumentation. - /// - /// being configured. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) - { - Guard.ThrowIfNull(builder); - - builder.AddInstrumentation(() => - { - // TODO: Allow this to be configured - var options = new KustoInstrumentationOptions(); - - var listener = new KustoMetricListener(options); - var handle = new ListenerHandle(listener); - - return handle; - }); - - builder.AddMeter(KustoActivitySourceHelper.MeterName); - - return builder; - } -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs new file mode 100644 index 0000000000..9c417d9189 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs @@ -0,0 +1,75 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Kusto.Cloud.Platform.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.Kusto; +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of Kusto instrumentation. +/// +public static class MeterProviderBuilderExtensions +{ + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) + => AddKustoInstrumentation(builder, options => { }); + + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// Action to configure the . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder, Action configureKustoInstrumentationOptions) + { + Guard.ThrowIfNull(configureKustoInstrumentationOptions); + + return AddKustoInstrumentation(builder, name: null, configureKustoInstrumentationOptions); + } + + // TODO: Revisit named options + + /// + /// Enables Kusto instrumentation. + /// + /// being configured. + /// The name of the options instance being configured. + /// Kusto instrumentation options. + /// The instance of to chain the calls. + private static MeterProviderBuilder AddKustoInstrumentation( + this MeterProviderBuilder builder, + string? name, + Action? configureOptions) + { + Guard.ThrowIfNull(builder); + name ??= Options.DefaultName; + + if (configureOptions != null) + { + builder.ConfigureServices(services => services.Configure(name, configureOptions)); + } + + builder.AddInstrumentation(sp => + { + var options = sp.GetRequiredService>().Get(name); + KustoInstrumentation.MetricOptions = options; + + KustoInstrumentation.InitializeMetrics(); + return KustoInstrumentation.HandleManager.AddMetricHandle(); + }); + + builder.AddMeter(KustoActivitySourceHelper.MeterName); + + return builder; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index 9f3a73d7fc..b4d7cf6b16 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -1,4 +1,4 @@ - + $(TargetFrameworksForLibraries) @@ -16,6 +16,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index ac73c3d56a..8a074d79e7 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Kusto.Cloud.Platform.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; @@ -18,7 +21,7 @@ public static class TracerProviderBuilderExtensions /// being configured. /// The instance of to chain the calls. public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) - => AddKustoInstrumentation(builder, new KustoInstrumentationOptions()); + => AddKustoInstrumentation(builder, options => { }); /// /// Enables Kusto instrumentation. @@ -31,35 +34,39 @@ public static TracerProviderBuilder AddKustoInstrumentation( Action configureKustoInstrumentationOptions) { Guard.ThrowIfNull(configureKustoInstrumentationOptions); - - var options = new KustoInstrumentationOptions(); - configureKustoInstrumentationOptions(options); - return AddKustoInstrumentation(builder, options); + return AddKustoInstrumentation(builder, name: null, configureKustoInstrumentationOptions); } + // TODO: Revisit named options + /// /// Enables Kusto instrumentation. /// /// being configured. - /// Kusto instrumentation options. + /// The name of the options instance being configured. + /// Kusto instrumentation options. /// The instance of to chain the calls. - public static TracerProviderBuilder AddKustoInstrumentation( + private static TracerProviderBuilder AddKustoInstrumentation( this TracerProviderBuilder builder, - KustoInstrumentationOptions options) + string? name, + Action? configureOptions) { Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(options); - builder.AddInstrumentation(() => + name ??= Options.DefaultName; + + if (configureOptions != null) + { + builder.ConfigureServices(services => services.Configure(name, configureOptions)); + } + + builder.AddInstrumentation(sp => { - if (options.RecordQueryText) - { - Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); - } + var options = sp.GetRequiredService>().Get(name); + KustoInstrumentation.TracingOptions = options; - var listener = new KustoTraceListener(options); - var handle = new ListenerHandle(listener); - return handle; + KustoInstrumentation.InitializeTracing(); + return KustoInstrumentation.HandleManager.AddTracingHandle(); }); builder.AddSource(KustoActivitySourceHelper.ActivitySourceName); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt index e62ca6fadb..a65afb050c 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt @@ -20,6 +20,9 @@ }, { server.address: {Hostname}:{Port} + }, + { + db.query.text: InvalidTable | take 10 } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt index fd5e326c8e..6bf06aa38c 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt index a1f785dd4d..5d681f9774 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt index c109b85177..e6132f67ca 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt index e68fa42ed9..43ee007b4e 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt index f6b20e2867..717d1f0c6c 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt index 3ef01504b3..6c0699301e 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt @@ -3,6 +3,7 @@ { DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, + Status: Ok, Tags: [ { db.system.name: kusto From c7637de131a9096998420777157dd8949c45fdb6 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Tue, 18 Nov 2025 17:17:47 -0800 Subject: [PATCH 10/46] Add test to verify when not registered --- .../KustoIntegrationTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index d81abe9a6b..a2695e8436 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -188,4 +188,51 @@ await Verify( .UseDirectory("Snapshots") .UseParameters(query, recordQueryText); } + + [EnabledOnDockerPlatformFact(DockerPlatform.Linux)] + public void NoInstrumentationRegistered_NoEventsEmitted() + { + // Arrange + var activities = new List(); + var metrics = new List(); + + // Create providers WITHOUT Kusto instrumentation + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(activities) + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(metrics) + .Build(); + + var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + ClientRequestId = "test-no-instrumentation", + }; + + // Act + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", "print number=42", crp); + + while (reader.Read()) + { + } + + tracerProvider.ForceFlush(); + meterProvider.ForceFlush(); + + // Assert - No Kusto activities or metrics should be emitted + var kustoActivities = activities + .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) + .ToList(); + + var kustoMetrics = metrics + .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) + .ToList(); + + Assert.Empty(kustoActivities); + Assert.Empty(kustoMetrics); + } } From 55d3a46d014c44234407c42fe54b9b8b155f21d6 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Tue, 18 Nov 2025 17:27:18 -0800 Subject: [PATCH 11/46] Clean up tests --- .../DataReaderExtensions.cs | 25 +++++++++++ .../KustoIntegrationTests.cs | 31 +++---------- .../KustoIntegrationTestsFixture.cs | 3 ++ ...tabases_recordQueryText=False.verified.txt | 42 ----------------- ...atabases_recordQueryText=True.verified.txt | 45 ------------------- ...version_recordQueryText=False.verified.txt | 42 ----------------- ... version_recordQueryText=True.verified.txt | 45 ------------------- 7 files changed, 35 insertions(+), 198 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs delete mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt delete mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt delete mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt delete mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs new file mode 100644 index 0000000000..f4eefc4fa7 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Data; +using System.Diagnostics; +using System.Text; +using Kusto.Data.Common; +using Kusto.Data.Net.Client; +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +internal static class DataReaderExtensions +{ + public static void Consume(this IDataReader reader) + { + while (reader.Read()) + { + } + } +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index a2695e8436..a0a5f1eb3f 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -1,9 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Data; using System.Diagnostics; using System.Text; -using Kusto.Data; using Kusto.Data.Common; using Kusto.Data.Net.Client; using OpenTelemetry.Instrumentation.Kusto.Implementation; @@ -25,11 +25,7 @@ public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) } [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] - [InlineData(".show version", true)] - [InlineData(".show databases", true)] [InlineData("print number=42", true)] - [InlineData(".show version", false)] - [InlineData(".show databases", false)] [InlineData("print number=42", false)] public async Task SuccessfulQueryTest(string query, bool recordQueryText) { @@ -46,7 +42,7 @@ public async Task SuccessfulQueryTest(string query, bool recordQueryText) .AddKustoInstrumentation() .Build(); - var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + var kcsb = this.fixture.ConnectionStringBuilder; using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); var crp = new ClientRequestProperties() @@ -56,12 +52,7 @@ public async Task SuccessfulQueryTest(string query, bool recordQueryText) }; using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); - - while (reader.Read()) - { - } - - Debugger.Break(); + reader.Consume(); tracerProvider.ForceFlush(); meterProvider.ForceFlush(); @@ -120,7 +111,7 @@ public async Task FailedQueryTest(string query, bool recordQueryText) .AddKustoInstrumentation() .Build(); - var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + var kcsb = this.fixture.ConnectionStringBuilder; using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); var crp = new ClientRequestProperties() @@ -133,12 +124,7 @@ public async Task FailedQueryTest(string query, bool recordQueryText) var exception = await Assert.ThrowsAnyAsync(async () => { using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); - - while (reader.Read()) - { - } - - Debugger.Break(); + reader.Consume(); await Task.CompletedTask; }); @@ -205,7 +191,7 @@ public void NoInstrumentationRegistered_NoEventsEmitted() .AddInMemoryExporter(metrics) .Build(); - var kcsb = new KustoConnectionStringBuilder(this.fixture.DatabaseContainer.GetConnectionString()); + var kcsb = this.fixture.ConnectionStringBuilder; using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); var crp = new ClientRequestProperties() @@ -215,10 +201,7 @@ public void NoInstrumentationRegistered_NoEventsEmitted() // Act using var reader = queryProvider.ExecuteQuery("NetDefaultDB", "print number=42", crp); - - while (reader.Read()) - { - } + reader.Consume(); tracerProvider.ForceFlush(); meterProvider.ForceFlush(); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs index b033a52e5c..4475380bfb 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTestsFixture.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Kusto.Data; using Testcontainers.Kusto; using Xunit; @@ -12,6 +13,8 @@ public sealed class KustoIntegrationTestsFixture : IAsyncLifetime public KustoContainer DatabaseContainer { get; } = CreateKusto(); + public KustoConnectionStringBuilder ConnectionStringBuilder => new(this.DatabaseContainer.GetConnectionString()); + public Task InitializeAsync() => this.DatabaseContainer.StartAsync(); public Task DisposeAsync() => this.DatabaseContainer.DisposeAsync().AsTask(); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt deleted file mode 100644 index 6bf06aa38c..0000000000 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=False.verified.txt +++ /dev/null @@ -1,42 +0,0 @@ -{ - Activities: [ - { - DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, - Status: Ok, - Tags: [ - { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - } - ], - OperationName: KD.RestClient.ExecuteQuery, - IdFormat: W3C - } - ], - Metrics: [ - { - Name: db.client.operation.duration, - Description: Duration of database client operations, - Unit: s, - Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative - } - ] -} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt deleted file mode 100644 index 5d681f9774..0000000000 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show databases_recordQueryText=True.verified.txt +++ /dev/null @@ -1,45 +0,0 @@ -{ - Activities: [ - { - DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, - Status: Ok, - Tags: [ - { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgZGF0YWJhc2Vz - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - }, - { - db.query.text: .show databases - } - ], - OperationName: KD.RestClient.ExecuteQuery, - IdFormat: W3C - } - ], - Metrics: [ - { - Name: db.client.operation.duration, - Description: Duration of database client operations, - Unit: s, - Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative - } - ] -} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt deleted file mode 100644 index e6132f67ca..0000000000 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=False.verified.txt +++ /dev/null @@ -1,42 +0,0 @@ -{ - Activities: [ - { - DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, - Status: Ok, - Tags: [ - { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgdmVyc2lvbg== - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - } - ], - OperationName: KD.RestClient.ExecuteQuery, - IdFormat: W3C - } - ], - Metrics: [ - { - Name: db.client.operation.duration, - Description: Duration of database client operations, - Unit: s, - Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative - } - ] -} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt deleted file mode 100644 index 43ee007b4e..0000000000 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=.show version_recordQueryText=True.verified.txt +++ /dev/null @@ -1,45 +0,0 @@ -{ - Activities: [ - { - DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, - Status: Ok, - Tags: [ - { - db.system.name: kusto - }, - { - kusto.client_request_id: LnNob3cgdmVyc2lvbg== - }, - { - db.operation.name: KD.RestClient.ExecuteQuery - }, - { - url.full: http://{Hostname}:{Port}/v1/rest/query - }, - { - server.address: {Hostname}:{Port} - }, - { - db.query.text: .show version - } - ], - OperationName: KD.RestClient.ExecuteQuery, - IdFormat: W3C - } - ], - Metrics: [ - { - Name: db.client.operation.duration, - Description: Duration of database client operations, - Unit: s, - Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative - } - ] -} \ No newline at end of file From 07c4bddedfe4699bf2b7bd5a57a0d36ebae7da22 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 19 Nov 2025 11:10:50 -0800 Subject: [PATCH 12/46] Update db.system.name to follow conventions --- .../Implementation/KustoActivitySourceHelper.cs | 2 +- .../KustoInstrumentationOptions.cs | 2 +- ...nvalidTable - take 10_recordQueryText=False.verified.txt | 6 +++--- ...InvalidTable - take 10_recordQueryText=True.verified.txt | 6 +++--- ...query=print number=42_recordQueryText=False.verified.txt | 6 +++--- ..._query=print number=42_recordQueryText=True.verified.txt | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs index 724778b258..c6c2303929 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs @@ -14,7 +14,7 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; /// internal static class KustoActivitySourceHelper { - public const string DbSystem = "kusto"; + public const string DbSystem = "azure.kusto"; public const string ActivitySourceName = "Kusto.Client"; public const string MeterName = "Kusto.Client"; public const string ClientRequestIdTagKey = "kusto.client_request_id"; diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index fdc9989477..2d83790c9b 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -10,7 +10,7 @@ public class KustoInstrumentationOptions { /// /// Gets or sets a value indicating whether the query text should be recorded as an attribute on the activity. - /// Default is false. + /// Default is . /// public bool RecordQueryText { get; set; } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt index e62ca6fadb..6e297064c9 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt @@ -1,4 +1,4 @@ -{ +{ Activities: [ { DisplayName: KD.RestClient.ExecuteQuery, @@ -7,7 +7,7 @@ StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', Tags: [ { - db.system.name: kusto + db.system.name: azure.kusto }, { kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== @@ -44,4 +44,4 @@ Type: SemanticException, HasMessage: true } -} \ No newline at end of file +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt index a65afb050c..755f107863 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt @@ -1,4 +1,4 @@ -{ +{ Activities: [ { DisplayName: KD.RestClient.ExecuteQuery, @@ -7,7 +7,7 @@ StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', Tags: [ { - db.system.name: kusto + db.system.name: azure.kusto }, { kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== @@ -47,4 +47,4 @@ Type: SemanticException, HasMessage: true } -} \ No newline at end of file +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt index 717d1f0c6c..5d535c671c 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -1,4 +1,4 @@ -{ +{ Activities: [ { DisplayName: KD.RestClient.ExecuteQuery, @@ -6,7 +6,7 @@ Status: Ok, Tags: [ { - db.system.name: kusto + db.system.name: azure.kusto }, { kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy @@ -39,4 +39,4 @@ Temporality: Cumulative } ] -} \ No newline at end of file +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt index 6c0699301e..8e5719800a 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt @@ -1,4 +1,4 @@ -{ +{ Activities: [ { DisplayName: KD.RestClient.ExecuteQuery, @@ -6,7 +6,7 @@ Status: Ok, Tags: [ { - db.system.name: kusto + db.system.name: azure.kusto }, { kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy @@ -42,4 +42,4 @@ Temporality: Cumulative } ] -} \ No newline at end of file +} From 2a39a243034274c09aa4275bef55289ce15e95b0 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 19 Nov 2025 12:05:40 -0800 Subject: [PATCH 13/46] Add query summarization and sanitization --- Directory.Packages.props | 1 + .../.publicApi/PublicAPI.Unshipped.txt | 1 - .../InstrumentationHandleManagerExtensions.cs | 5 - .../Implementation/KustoProcessor.cs | 203 ++++++++++ .../Implementation/KustoStatementInfo.cs | 17 + .../Implementation/KustoTraceListener.cs | 32 +- .../Implementation/StringBuilderExtensions.cs | 34 ++ .../MeterProviderBuilderExtensions.cs | 2 - ...OpenTelemetry.Instrumentation.Kusto.csproj | 9 +- .../TracerProviderBuilderExtensions.cs | 1 - .../DataReaderExtensions.cs | 11 +- .../KustoQueryParserTests.cs | 378 ++++++++++++++++++ ...lemetry.Instrumentation.Kusto.Tests.csproj | 2 + ...take 10_recordQueryText=False.verified.txt | 7 +- ... take 10_recordQueryText=True.verified.txt | 9 +- ...mber=42_recordQueryText=False.verified.txt | 5 +- ...umber=42_recordQueryText=True.verified.txt | 9 +- 17 files changed, 692 insertions(+), 34 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ddc5277d1e..0290442c5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -84,6 +84,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index edf3f8a737..7d716fdc98 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -8,5 +8,4 @@ OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions! options) -> OpenTelemetry.Trace.TracerProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs index 9f8c735de3..3d9df2db91 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs @@ -1,11 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Collections.Concurrent; -using System.Diagnostics; -using OpenTelemetry.Trace; -using KustoUtils = Kusto.Cloud.Platform.Utils; - namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal static class InstrumentationHandleManagerExtensions diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs new file mode 100644 index 0000000000..685c37915f --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -0,0 +1,203 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using Kusto.Language; +using Kusto.Language.Editor; +using Kusto.Language.Symbols; +using Kusto.Language.Syntax; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class KustoProcessor +{ + private enum ReplacementKind + { + Placeholder, + Remove, + } + + public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, string query) + { + var code = KustoCode.ParseAndAnalyze(query); + + string? summarized = null; + string? sanitized = null; + + if (shouldSummarize) + { + summarized = Summarize(code); + } + + if (shouldSanitize) + { + sanitized = Sanitize(code); + } + + return new KustoStatementInfo(summarized, sanitized); + } + + private static string Sanitize(KustoCode code) + { + // Collect nodes that need replacements + var collector = new SanitizerVisitor(); + code.Syntax.Accept(collector); + + // Build edits + var edits = new List(); + foreach (var replacement in collector.Replacements) + { + var edit = replacement.Kind switch + { + ReplacementKind.Placeholder => TextEdit.Replacement(replacement.Element.TextStart, replacement.Element.Width, "?"), + ReplacementKind.Remove => TextEdit.Deletion(replacement.Element.TextStart, replacement.Element.Width), + _ => throw new NotSupportedException($"Unexpected replacement kind: {replacement.Kind}"), + }; + + edits.Add(edit); + } + + // Apply edits to text + var text = new EditString(code.Text); + if (edits.Count == 0 || !text.CanApplyAll(edits)) + { + return code.Text; + } + + var newText = text.ApplyAll(edits); + return newText; + } + + private static string Summarize(KustoCode code) + { + var walker = new SummarizerVisitor(); + code.Syntax.Accept(walker); + + var sb = new StringBuilder(); + foreach (var segment in walker.Builder) + { + sb.Append(segment).Append(' '); + } + + sb.TrimEnd(); + return sb.ToString(0, Math.Min(255, sb.Length)); + } + + private readonly struct Replacement + { + public Replacement(SyntaxElement element, ReplacementKind kind) + { + this.Element = element; + this.Kind = kind; + } + + public SyntaxElement Element { get; } + + public ReplacementKind Kind { get; } + } + + private sealed class SanitizerVisitor : DefaultSyntaxVisitor + { + public readonly List Replacements = []; + + public override void VisitLiteralExpression(LiteralExpression node) => this.Replacements.Add(new Replacement(node, ReplacementKind.Placeholder)); + + public override void VisitDynamicExpression(DynamicExpression node) => this.Replacements.Add(new Replacement(node, ReplacementKind.Placeholder)); + + public override void VisitPrefixUnaryExpression(PrefixUnaryExpression node) + { + this.Replacements.Add(new Replacement(node.Operator, ReplacementKind.Remove)); + base.VisitPrefixUnaryExpression(node); + } + + protected override void DefaultVisit(SyntaxNode node) => this.VisitChildren(node); + + private void VisitChildren(SyntaxNode node) + { + if (node != null) + { + for (int i = 0; i < node.ChildCount; i++) + { + if (node.GetChild(i) is SyntaxNode child) + { + child.Accept(this); + } + } + } + } + } + + private sealed class SummarizerVisitor : DefaultSyntaxVisitor + { + public readonly List Builder = []; + + public override void VisitPipeExpression(PipeExpression node) + { + node.Expression.Accept(this); + this.Builder.Add(node.Bar.Text); + node.Operator.Accept(this); + } + + public override void VisitNameReference(NameReference node) + { + if (node.ResultType is TableSymbol ts) + { + this.Builder.Add(ts.Name); + } + else if (node.ResultType is ErrorSymbol) + { + this.Builder.Add(node.ToString(IncludeTrivia.SingleLine)); + } + } + + public override void VisitFunctionCallExpression(FunctionCallExpression node) + { + if (node.Name.SimpleName == "materialized_view") + { + this.Builder.Add(node.ToString(IncludeTrivia.SingleLine)); + } + } + + public override void VisitDataTableExpression(DataTableExpression node) => this.Builder.Add(node.DataTableKeyword.Text); + + public override void VisitCustomCommand(CustomCommand node) => this.Builder.Add(node.DotToken + node.Custom.GetFirstToken().ToString(IncludeTrivia.SingleLine)); + + protected override void DefaultVisit(SyntaxNode node) + { + if (node is QueryOperator qo) + { + this.VisitQueryOperator(qo); + } + else + { + this.VisitChildren(node); + } + } + + private void VisitQueryOperator(QueryOperator node) + { + if (node is BadQueryOperator) + { + return; + } + + this.Builder.Add(node.GetFirstToken().ToString(IncludeTrivia.SingleLine)); + + this.VisitChildren(node); + } + + private void VisitChildren(SyntaxNode node) + { + if (node != null) + { + for (int i = 0; i < node.ChildCount; i++) + { + if (node.GetChild(i) is SyntaxNode child) + { + child.Accept(this); + } + } + } + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs new file mode 100644 index 0000000000..bf63107edb --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal readonly struct KustoStatementInfo +{ + public KustoStatementInfo(string? summarized, string? sanitized) + { + this.Summarized = summarized; + this.Sanitized = sanitized; + } + + public string? Summarized { get; } + + public string? Sanitized { get; } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 32ec7aa04f..b6ed714fbf 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -126,14 +126,36 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) } } - // TODO: Consider adding summary - if (KustoInstrumentation.TracingOptions.RecordQueryText) + // Extract and parse query text + var text = ExtractValueBetween(message, "text=".AsSpan(), Environment.NewLine.AsSpan()); + + if (!text.IsEmpty) { - var text = ExtractValueBetween(message, "text=".AsSpan(), Environment.NewLine.AsSpan()); - if (!text.IsEmpty) + var queryText = text.ToString(); + var info = KustoProcessor.Process(shouldSummarize: true, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, queryText); + + // Set sanitized query text if configured + if (KustoInstrumentation.TracingOptions.RecordQueryText) + { + activity.SetTag(SemanticConventions.AttributeDbQueryText, info.Sanitized); + } + + // Set query summary and use it as display name per spec + if (!string.IsNullOrEmpty(info.Summarized)) { - activity.SetTag(SemanticConventions.AttributeDbQueryText, text.ToString()); + activity.SetTag(SemanticConventions.AttributeDbQuerySummary, info.Summarized); + activity.DisplayName = info.Summarized!; } + else + { + // Fall back to operation name if no summary available + activity.DisplayName = operationName; + } + } + else + { + // Fall back to operation name if no query text + activity.DisplayName = operationName; } } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs new file mode 100644 index 0000000000..f6fd8eeae4 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs @@ -0,0 +1,34 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class StringBuilderExtensions +{ + public static StringBuilder TrimEnd(this StringBuilder sb) + { + if (sb.Length == 0) + { + return sb; + } + + int i = sb.Length - 1; + + for (; i >= 0; i--) + { + if (!char.IsWhiteSpace(sb[i])) + { + break; + } + } + + if (i < sb.Length - 1) + { + sb.Length = i + 1; + } + + return sb; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs index 9c417d9189..70d19599ec 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs @@ -1,13 +1,11 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Kusto.Cloud.Platform.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; -using OpenTelemetry.Trace; namespace OpenTelemetry.Metrics; diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index b4d7cf6b16..3ff965bf20 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -1,4 +1,4 @@ - + $(TargetFrameworksForLibraries) @@ -6,8 +6,14 @@ OpenTelemetry Kusto Instrumentation. $(PackageTags);Kusto;AzureDataExplorer Instrumentation.Kusto- + + false + + + + @@ -16,6 +22,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 8a074d79e7..5dd4814a99 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Kusto.Cloud.Platform.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs index f4eefc4fa7..132aa4be0d 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DataReaderExtensions.cs @@ -2,15 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Data; -using System.Diagnostics; -using System.Text; -using Kusto.Data.Common; -using Kusto.Data.Net.Client; -using OpenTelemetry.Instrumentation.Kusto.Implementation; -using OpenTelemetry.Metrics; -using OpenTelemetry.Tests; -using OpenTelemetry.Trace; -using Xunit; namespace OpenTelemetry.Instrumentation.Kusto.Tests; @@ -22,4 +13,4 @@ public static void Consume(this IDataReader reader) { } } -} \ No newline at end of file +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs new file mode 100644 index 0000000000..0063aee8b7 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs @@ -0,0 +1,378 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class KustoQueryParserTests +{ + public static TheoryData QuerySummaryTestCases => new() + { + // Null / empty / invalid + // NOTE: In these cases, there's no objectively correct answer, so the main goal + // is to ensure we handle error cases gracefully + { + string.Empty, + string.Empty, + string.Empty + }, + { + " \t\n ", + string.Empty, + " \t\n " + }, + { + "this is not a valid query @#$%", + "this is a valid query", + "this is not a valid query @#$%" + }, + { + "StormEvents |", + "StormEvents |", + "StormEvents |" + }, + + // Simple table reference + { + "StormEvents", + "StormEvents", + "StormEvents" + }, + + // Print statement + { + "print number=42", + "print", + "print number=?" + }, + + // Pipes + { + "StormEvents | where State == 'FLORIDA'", + "StormEvents | where", + "StormEvents | where State == ?" + }, + { + "StormEvents | project State, EventType", + "StormEvents | project", + "StormEvents | project State, EventType" + }, + { + "StormEvents | summarize count() by State", + "StormEvents | summarize", + "StormEvents | summarize count() by State" + }, + { + "StormEvents | where State == 'FLORIDA' | project State, EventType | take 10", + "StormEvents | where | project | take", + "StormEvents | where State == ? | project State, EventType | take ?" + }, + { + "StormEvents | where State == 'CA' | extend NewCol = 1 | project State | summarize count() | order by count_", + "StormEvents | where | extend | project | summarize | order", + "StormEvents | where State == ? | extend NewCol = ? | project State | summarize count() | order by count_" + }, + + // Database function + { + "database('SampleDB').StormEvents", + "StormEvents", + "database(?).StormEvents" + }, + + // Let statement + { + "let threshold = 5; StormEvents | where DamageProperty > threshold", + "StormEvents | where", + "let threshold = ?; StormEvents | where DamageProperty > threshold" + }, + { + "let x = 10; let y = 20; StormEvents | take x", + "StormEvents | take", + "let x = ?; let y = ?; StormEvents | take x" + }, + + // Nested queries + { + "StormEvents | union OtherEvents", + "StormEvents | union OtherEvents", + "StormEvents | union OtherEvents" + }, + { + "StormEvents | join kind=inner (PopulationData) on State", + "StormEvents | join PopulationData", + "StormEvents | join kind=? (PopulationData) on State" + }, + { + "let threshold = 1000;\nStormEvents\n| where DamageProperty > threshold\n| join kind=inner (\n PopulationData\n | where Year == 2020\n) on State\n| summarize TotalDamage = sum(DamageProperty) by State\n| top 10 by TotalDamage", + "StormEvents | where | join PopulationData | where | summarize | top", + "let threshold = ?;\nStormEvents\n| where DamageProperty > threshold\n| join kind=? (\n PopulationData\n | where Year == ?\n) on State\n| summarize TotalDamage = sum(DamageProperty) by State\n| top ? by TotalDamage" + }, + { + "StormEvents | union WeatherEvents | where State == 'TX'", + "StormEvents | union WeatherEvents | where", + "StormEvents | union WeatherEvents | where State == ?" + }, + + // Control command + { + ".show databases", + ".show", + ".show databases" + }, + + // Range + { + "range x from 1 to 10 step 1", + "range", + "range x from ? to ? step ?" + }, + { + "StormEvents | where Value between (10 .. 100)", + "StormEvents | where", + "StormEvents | where Value between (? .. ?)" + }, + + // DataTable + { + "datatable(name:string, age:int) ['Alice', 30, 'Bob', 25]", + "datatable", + "datatable(name:string, age:int) [?, ?, ?, ?]" + }, + + // Query with newlines + { + "StormEvents\n| where State == 'FLORIDA'\n| project State, EventType\n| take 10", + "StormEvents | where | project | take", + "StormEvents\n| where State == ?\n| project State, EventType\n| take ?" + }, + + // Query with tabs + { + "StormEvents\t|\twhere\tState\t==\t'FLORIDA'", + "StormEvents | where", + "StormEvents\t|\twhere\tState\t==\t?" + }, + + // Comments + // NOTE: Ideally comments would be stripped from sanitized queries, but Kusto.Language does not easily allow for + // stripping embedded comments, so codifying the behavior for now. If this becomes a problem we can revisit. + { + "// Single line comment\nStormEvents | where State == 'TX'", + "StormEvents | where", + "// Single line comment\nStormEvents | where State == ?" + }, + { + "StormEvents | take 10 // Get first 10 rows", + "StormEvents | take", + "StormEvents | take ? // Get first 10 rows" + }, + + // Number parsing + { + "StormEvents | where Temperature < -10", + "StormEvents | where", + "StormEvents | where Temperature < ?" + }, + { + "StormEvents | where Value == -42.5", + "StormEvents | where", + "StormEvents | where Value == ?" + }, + { + "StormEvents | where Count > +100", + "StormEvents | where", + "StormEvents | where Count > ?" + }, + { + "StormEvents | where Value > 1.5e10", + "StormEvents | where", + "StormEvents | where Value > ?" + }, + { + "StormEvents | where Value < 3.14E-5", + "StormEvents | where", + "StormEvents | where Value < ?" + }, + { + "StormEvents | where Value == -2.5e+8", + "StormEvents | where", + "StormEvents | where Value == ?" + }, + { + "StormEvents | where Price == 123.456", + "StormEvents | where", + "StormEvents | where Price == ?" + }, + + // Mixed string and numeric literals + { + "StormEvents | where State == 'FL' and Temp > 90", + "StormEvents | where", + "StormEvents | where State == ? and Temp > ?" + }, + + // Double-quoted strings + { + "print text = \"Hello World\"", + "print", + "print text = ?" + }, + + // Empty strings + { + "StormEvents | where State != ''", + "StormEvents | where", + "StormEvents | where State != ?" + }, + + // Nested parentheses + { + "StormEvents | where (State == 'CA' and (Temp > 80))", + "StormEvents | where", + "StormEvents | where (State == ? and (Temp > ?))" + }, + + // Boolean literals + { + "StormEvents | where IsActive == true", + "StormEvents | where", + "StormEvents | where IsActive == ?" + }, + { + "StormEvents | where IsDeleted == false", + "StormEvents | where", + "StormEvents | where IsDeleted == ?" + }, + { + "print flag=true", + "print", + "print flag=?" + }, + { + "StormEvents | extend Active = false", + "StormEvents | extend", + "StormEvents | extend Active = ?" + }, + + // DateTime literals + { + "StormEvents | where StartTime > datetime(2020-01-01)", + "StormEvents | where", + "StormEvents | where StartTime > ?" + }, + { + "StormEvents | where EventTime >= datetime('2021-05-15T10:30:00Z')", + "StormEvents | where", + "StormEvents | where EventTime >= ?" + }, + { + "StormEvents | where timestamp > datetime(2023-12-25)", + "StormEvents | where", + "StormEvents | where timestamp > ?" + }, + + // TimeSpan/duration literals + { + "StormEvents | where Duration > 1h", + "StormEvents | where", + "StormEvents | where Duration > ?" + }, + { + "StormEvents | where StartTime < ago(7d)", + "StormEvents | where", + "StormEvents | where StartTime < ago(?)" + }, + { + "StormEvents | where TimeDiff < 30m", + "StormEvents | where", + "StormEvents | where TimeDiff < ?" + }, + { + "StormEvents | where Duration == 2.5h", + "StormEvents | where", + "StormEvents | where Duration == ?" + }, + + // GUID literals + { + "StormEvents | where Id == guid(12345678-1234-1234-1234-123456789012)", + "StormEvents | where", + "StormEvents | where Id == ?" + }, + { + "Users | where UserId == guid('a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d')", + "Users | where", + "Users | where UserId == ?" + }, + + // Dynamic/JSON literals + { + "StormEvents | where Data == dynamic({\"key\":\"value\"})", + "StormEvents | where", + "StormEvents | where Data == ?" + }, + { + "StormEvents | extend Props = dynamic(['a', 'b', 'c'])", + "StormEvents | extend", + "StormEvents | extend Props = ?" + }, + { + "print obj=dynamic({\"x\":1,\"y\":2})", + "print", + "print obj=?" + }, + + // Binary/Hexadecimal literals + { + "StormEvents | where Flags == 0x1F", + "StormEvents | where", + "StormEvents | where Flags == ?" + }, + { + "StormEvents | where Mask == 0xFF00", + "StormEvents | where", + "StormEvents | where Mask == ?" + }, + { + "print hex=0xDEADBEEF", + "print", + "print hex=?" + }, + + // Interval literals + { + "StormEvents | where StartTime between (datetime(2007-07-27) .. ago(1d))", + "StormEvents | where", + "StormEvents | where StartTime between (? .. ago(?))" + }, + + // Materialized view + // NOTE: Ideally the summarizer would strip "Condition", but it this context it is ambiguous with a table reference. Given the prevelance of this type of query, + // codifying the behavior for now. If this becomes a problem we can revisit. + { + "database('*').materialized_view('ViewName') | where Condition == 'Value'", + "materialized_view('ViewName') | where Condition", + "database(?).materialized_view(?) | where Condition == ?" + }, + + // Long query - tests 255 character truncation + { + "StormEvents | where State == 'CALIFORNIA' | extend NewColumn1 = 1 | extend NewColumn2 = 2 | extend NewColumn3 = 3 | extend NewColumn4 = 4 | extend NewColumn5 = 5 | extend NewColumn6 = 6 | extend NewColumn7 = 7 | extend NewColumn8 = 8 | extend NewColumn9 = 9 | extend NewColumn10 = 10 | extend NewColumn11 = 11 | extend NewColumn12 = 12 | extend NewColumn13 = 13 | extend NewColumn14 = 14 | extend NewColumn15 = 15 | extend NewColumn16 = 16 | extend NewColumn17 = 17 | extend NewColumn18 = 18 | extend NewColumn19 = 19 | extend NewColumn20 = 20 | extend NewColumn21 = 21 | extend NewColumn22 = 22 | extend NewColumn23 = 23 | extend NewColumn24 = 24 | extend NewColumn25 = 25 | extend NewColumn26 = 26 | extend NewColumn27 = 27", + "StormEvents | where | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend | extend |", + "StormEvents | where State == ? | extend NewColumn1 = ? | extend NewColumn2 = ? | extend NewColumn3 = ? | extend NewColumn4 = ? | extend NewColumn5 = ? | extend NewColumn6 = ? | extend NewColumn7 = ? | extend NewColumn8 = ? | extend NewColumn9 = ? | extend NewColumn10 = ? | extend NewColumn11 = ? | extend NewColumn12 = ? | extend NewColumn13 = ? | extend NewColumn14 = ? | extend NewColumn15 = ? | extend NewColumn16 = ? | extend NewColumn17 = ? | extend NewColumn18 = ? | extend NewColumn19 = ? | extend NewColumn20 = ? | extend NewColumn21 = ? | extend NewColumn22 = ? | extend NewColumn23 = ? | extend NewColumn24 = ? | extend NewColumn25 = ? | extend NewColumn26 = ? | extend NewColumn27 = ?" + }, + }; + + [Theory] + [MemberData(nameof(QuerySummaryTestCases))] + public void GenerateQuerySummary_ReturnsExpectedSummary(string query, string? expectedSummary, string? expectedSanitizedQuery) + { + var info = KustoProcessor.Process(shouldSummarize: true, shouldSanitize: true, query); + + Assert.Equal(expectedSummary, info.Summarized); + Assert.Equal(expectedSanitizedQuery, info.Sanitized); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj index 8c2741545a..d8d99b4798 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj @@ -3,6 +3,8 @@ $(SupportedNetTargets) $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) + false + true diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt index 6e297064c9..705a5921b5 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: KD.RestClient.ExecuteQuery, + DisplayName: InvalidTable | take, Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', @@ -20,6 +20,9 @@ }, { server.address: {Hostname}:{Port} + }, + { + db.query.summary: InvalidTable | take } ], OperationName: KD.RestClient.ExecuteQuery, @@ -44,4 +47,4 @@ Type: SemanticException, HasMessage: true } -} +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt index 755f107863..3369ebc297 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: KD.RestClient.ExecuteQuery, + DisplayName: InvalidTable | take, Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', @@ -22,7 +22,10 @@ server.address: {Hostname}:{Port} }, { - db.query.text: InvalidTable | take 10 + db.query.text: InvalidTable | take ? + }, + { + db.query.summary: InvalidTable | take } ], OperationName: KD.RestClient.ExecuteQuery, @@ -47,4 +50,4 @@ Type: SemanticException, HasMessage: true } -} +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt index 5d535c671c..6eb7273a36 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: KD.RestClient.ExecuteQuery, + DisplayName: print, Name: Kusto.Client, Status: Ok, Tags: [ @@ -19,6 +19,9 @@ }, { server.address: {Hostname}:{Port} + }, + { + db.query.summary: print } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt index 8e5719800a..95e050e7f2 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: KD.RestClient.ExecuteQuery, + DisplayName: print, Name: Kusto.Client, Status: Ok, Tags: [ @@ -21,7 +21,10 @@ server.address: {Hostname}:{Port} }, { - db.query.text: print number=42 + db.query.text: print number=? + }, + { + db.query.summary: print } ], OperationName: KD.RestClient.ExecuteQuery, @@ -42,4 +45,4 @@ Temporality: Cumulative } ] -} +} \ No newline at end of file From 8476dd6d7e7d73b9fa61bb9015c5d512a0808374 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Thu, 20 Nov 2025 16:55:11 -0800 Subject: [PATCH 14/46] Add Benchmarks project --- opentelemetry-dotnet-contrib.slnx | 1 + ...OpenTelemetry.Instrumentation.Kusto.csproj | 1 + .../KustoProcessorBenchmarks.cs | 23 ++++++++++++ .../KustoProcessorProfilingBenchmarks.cs | 21 +++++++++++ ...ry.Instrumentation.Kusto.Benchmarks.csproj | 25 +++++++++++++ .../Program.cs | 23 ++++++++++++ .../README.md | 35 +++++++++++++++++++ 7 files changed, 129 insertions(+) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorBenchmarks.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorProfilingBenchmarks.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md diff --git a/opentelemetry-dotnet-contrib.slnx b/opentelemetry-dotnet-contrib.slnx index e82f4b94f7..ae15e37ea6 100644 --- a/opentelemetry-dotnet-contrib.slnx +++ b/opentelemetry-dotnet-contrib.slnx @@ -268,6 +268,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index 3ff965bf20..df5cbd8072 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -12,6 +12,7 @@ + + $(SupportedNetTargets) + Exe + false + true + + + + + + + + + + + + + + $(DefineConstants);DISABLE_PROFILER_AGENT_CONFIG + + + diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs new file mode 100644 index 0000000000..ea2e249c65 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using BenchmarkDotNet.Running; + +namespace OpenTelemetry.Instrumentation.Kusto.Benchmarks; + +internal class Program +{ + private static void Main(string[] args) + { + if (Debugger.IsAttached) + { + var benchmarks = new KustoProcessorProfilingBenchmarks(); + benchmarks.ProcessSummarizeAndSanitize(); + } + else + { + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md new file mode 100644 index 0000000000..7de4f48999 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -0,0 +1,35 @@ +# OpenTelemetry.Instrumentation.Kusto.Benchmarks + +This project contains benchmarks for the OpenTelemetry Kusto instrumentation library. + +## Running the Benchmarks + +To run all benchmarks: + +```bash +dotnet run --configuration Release --framework net10.0 --project test\OpenTelemetry.Instrumentation.Kusto.Benchmarks +``` + +Then choose the benchmark class that you want to run by entering the required +option number from the list of options shown on the Console window. + +> [!TIP] +> The Profiling benchmarks are designed to run quickly and use the Visual Studio diagnosers to gather performance data. + +## Results + +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.7092/24H2/2024Update/HudsonValley) +Intel Core i9-10940X CPU 3.30GHz (Max: 3.31GHz), 1 CPU, 28 logical and 14 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + + +``` +| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | +|---------------------------- |---------:|---------:|---------:|---------:|-------:|-------:|----------:| +| ProcessSummarizeAndSanitize | 31.85 μs | 0.941 μs | 2.686 μs | 30.64 μs | 4.8828 | 0.0610 | 48.38 KB | +| ProcessSummarizeOnly | 26.73 μs | 0.531 μs | 1.311 μs | 26.47 μs | 4.7607 | 0.4272 | 47.25 KB | +| ProcessSanitizeOnly | 25.91 μs | 0.509 μs | 0.778 μs | 25.75 μs | 4.8523 | - | 47.74 KB | From 294f5c62ad7474affa576657b114073e02d7ef2b Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 24 Nov 2025 11:39:51 -0800 Subject: [PATCH 15/46] Use record structs --- .../Implementation/KustoStatementInfo.cs | 13 +------------ .../OpenTelemetry.Instrumentation.Kusto.csproj | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs index bf63107edb..b4f83b691b 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoStatementInfo.cs @@ -3,15 +3,4 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; -internal readonly struct KustoStatementInfo -{ - public KustoStatementInfo(string? summarized, string? sanitized) - { - this.Summarized = summarized; - this.Sanitized = sanitized; - } - - public string? Summarized { get; } - - public string? Sanitized { get; } -} +internal readonly record struct KustoStatementInfo(string? Summarized, string? Sanitized); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index df5cbd8072..89bdd11932 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -33,6 +33,7 @@ + From 06afa83993babb37804282b29030e0e9060c2a73 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 24 Nov 2025 11:40:16 -0800 Subject: [PATCH 16/46] Perf: Use a truncating string builder --- .../Implementation/KustoProcessor.cs | 53 ++++-- .../Implementation/TruncatingStringBuilder.cs | 104 +++++++++++ .../README.md | 12 +- .../TruncatingStringBuilderTests.cs | 176 ++++++++++++++++++ 4 files changed, 320 insertions(+), 25 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/TruncatingStringBuilder.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/TruncatingStringBuilderTests.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index 685c37915f..aba4b2f03d 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Text; using Kusto.Language; using Kusto.Language.Editor; using Kusto.Language.Symbols; @@ -70,17 +69,9 @@ private static string Sanitize(KustoCode code) private static string Summarize(KustoCode code) { - var walker = new SummarizerVisitor(); + using var walker = new SummarizerVisitor(); code.Syntax.Accept(walker); - - var sb = new StringBuilder(); - foreach (var segment in walker.Builder) - { - sb.Append(segment).Append(' '); - } - - sb.TrimEnd(); - return sb.ToString(0, Math.Min(255, sb.Length)); + return walker.GetSummary(); } private readonly struct Replacement @@ -127,14 +118,15 @@ private void VisitChildren(SyntaxNode node) } } - private sealed class SummarizerVisitor : DefaultSyntaxVisitor + private sealed class SummarizerVisitor : DefaultSyntaxVisitor, IDisposable { - public readonly List Builder = []; + private readonly TruncatingStringBuilder builder = new(); public override void VisitPipeExpression(PipeExpression node) { node.Expression.Accept(this); - this.Builder.Add(node.Bar.Text); + this.builder.Append(node.Bar.Text); + this.builder.Append(' '); node.Operator.Accept(this); } @@ -142,11 +134,13 @@ public override void VisitNameReference(NameReference node) { if (node.ResultType is TableSymbol ts) { - this.Builder.Add(ts.Name); + this.builder.Append(ts.Name); + this.builder.Append(' '); } else if (node.ResultType is ErrorSymbol) { - this.Builder.Add(node.ToString(IncludeTrivia.SingleLine)); + this.builder.Append(node.ToString(IncludeTrivia.SingleLine)); + this.builder.Append(' '); } } @@ -154,13 +148,31 @@ public override void VisitFunctionCallExpression(FunctionCallExpression node) { if (node.Name.SimpleName == "materialized_view") { - this.Builder.Add(node.ToString(IncludeTrivia.SingleLine)); + this.builder.Append(node.ToString(IncludeTrivia.SingleLine)); + this.builder.Append(' '); } } - public override void VisitDataTableExpression(DataTableExpression node) => this.Builder.Add(node.DataTableKeyword.Text); + public override void VisitDataTableExpression(DataTableExpression node) + { + this.builder.Append(node.DataTableKeyword.Text); + this.builder.Append(' '); + } + + public override void VisitCustomCommand(CustomCommand node) + { + this.builder.Append(node.DotToken.Text); + this.builder.Append(node.Custom.GetFirstToken().Text); + this.builder.Append(' '); + } + + public string GetSummary() + { + this.builder.TrimEnd(); + return this.builder.ToString(); + } - public override void VisitCustomCommand(CustomCommand node) => this.Builder.Add(node.DotToken + node.Custom.GetFirstToken().ToString(IncludeTrivia.SingleLine)); + public void Dispose() => this.builder.Dispose(); protected override void DefaultVisit(SyntaxNode node) { @@ -181,7 +193,8 @@ private void VisitQueryOperator(QueryOperator node) return; } - this.Builder.Add(node.GetFirstToken().ToString(IncludeTrivia.SingleLine)); + this.builder.Append(node.GetFirstToken().ToString(IncludeTrivia.SingleLine)); + this.builder.Append(' '); this.VisitChildren(node); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TruncatingStringBuilder.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TruncatingStringBuilder.cs new file mode 100644 index 0000000000..20783cc6f8 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TruncatingStringBuilder.cs @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Buffers; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +/// +/// A high-performance string builder that uses ArrayPool and truncates at a maximum length. +/// Once truncation occurs, all future append operations are ignored. +/// +internal sealed class TruncatingStringBuilder : IDisposable +{ + private const int MaxLength = 255; + private char[]? buffer; + private int position; + private bool isTruncated; + + public TruncatingStringBuilder() + { + this.buffer = ArrayPool.Shared.Rent(MaxLength); + this.position = 0; + this.isTruncated = false; + } + + public int Length => this.position; + + public bool IsTruncated => this.isTruncated; + + public void Append(string value) + { + if (this.isTruncated || string.IsNullOrEmpty(value)) + { + return; + } + + this.Append(value.AsSpan()); + } + + public void Append(ReadOnlySpan value) + { + if (this.isTruncated || value.IsEmpty || this.buffer == null) + { + return; + } + + if (this.position + value.Length > MaxLength) + { + this.isTruncated = true; + return; + } + + value.CopyTo(this.buffer.AsSpan(this.position)); + this.position += value.Length; + } + + public void Append(char value) + { + if (this.isTruncated || this.buffer == null) + { + return; + } + + if (this.position + 1 > MaxLength) + { + this.isTruncated = true; + return; + } + + this.buffer[this.position++] = value; + } + + public void TrimEnd() + { + if (this.buffer == null || this.position == 0) + { + return; + } + + while (this.position > 0 && char.IsWhiteSpace(this.buffer[this.position - 1])) + { + this.position--; + } + } + + public override string ToString() + { + if (this.buffer == null || this.position == 0) + { + return string.Empty; + } + + return new string(this.buffer, 0, this.position); + } + + public void Dispose() + { + if (this.buffer != null) + { + ArrayPool.Shared.Return(this.buffer); + this.buffer = null; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 7de4f48999..249119d108 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -18,6 +18,8 @@ option number from the list of options shown on the Console window. ## Results +// TODO: Includes private fixes in Kusto-Query-Langugage + ``` BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.7092/24H2/2024Update/HudsonValley) @@ -28,8 +30,8 @@ Intel Core i9-10940X CPU 3.30GHz (Max: 3.31GHz), 1 CPU, 28 logical and 14 physic ``` -| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | -|---------------------------- |---------:|---------:|---------:|---------:|-------:|-------:|----------:| -| ProcessSummarizeAndSanitize | 31.85 μs | 0.941 μs | 2.686 μs | 30.64 μs | 4.8828 | 0.0610 | 48.38 KB | -| ProcessSummarizeOnly | 26.73 μs | 0.531 μs | 1.311 μs | 26.47 μs | 4.7607 | 0.4272 | 47.25 KB | -| ProcessSanitizeOnly | 25.91 μs | 0.509 μs | 0.778 μs | 25.75 μs | 4.8523 | - | 47.74 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|---------------------------- |---------:|---------:|---------:|-------:|-------:|----------:| +| ProcessSummarizeAndSanitize | 25.24 μs | 0.490 μs | 0.459 μs | 2.8381 | - | 27.94 KB | +| ProcessSummarizeOnly | 22.30 μs | 0.436 μs | 0.408 μs | 2.7161 | 0.2136 | 26.81 KB | +| ProcessSanitizeOnly | 23.91 μs | 0.417 μs | 0.528 μs | 2.8076 | 0.1526 | 27.73 KB | diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TruncatingStringBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TruncatingStringBuilderTests.cs new file mode 100644 index 0000000000..4d501eb0d9 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TruncatingStringBuilderTests.cs @@ -0,0 +1,176 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class TruncatingStringBuilderTests +{ + [Fact] + public void Append_String_AppendsValue() + { + using var builder = new TruncatingStringBuilder(); + builder.Append("hello"); + builder.Append(" "); + builder.Append("world"); + + Assert.Equal("hello world", builder.ToString()); + Assert.Equal(11, builder.Length); + Assert.False(builder.IsTruncated); + } + + [Fact] + public void Append_Char_AppendsValue() + { + using var builder = new TruncatingStringBuilder(); + builder.Append('a'); + builder.Append('b'); + builder.Append('c'); + + Assert.Equal("abc", builder.ToString()); + Assert.Equal(3, builder.Length); + Assert.False(builder.IsTruncated); + } + + [Fact] + public void Append_Span_AppendsValue() + { + using var builder = new TruncatingStringBuilder(); + builder.Append("hello".AsSpan()); + builder.Append(" ".AsSpan()); + builder.Append("world".AsSpan()); + + Assert.Equal("hello world", builder.ToString()); + Assert.Equal(11, builder.Length); + Assert.False(builder.IsTruncated); + } + + [Fact] + public void Append_ExceedsMaxLength_DoesNotAppendPartialValue() + { + using var builder = new TruncatingStringBuilder(); + + // Fill up to 250 characters + var segment = new string('a', 50); + for (int i = 0; i < 5; i++) + { + builder.Append(segment); + } + + Assert.Equal(250, builder.Length); + Assert.False(builder.IsTruncated); + + // Try to append 10 more characters (would exceed 255) + builder.Append("1234567890"); + + // Should be truncated and not append partial value + Assert.Equal(250, builder.Length); + Assert.True(builder.IsTruncated); + Assert.DoesNotContain("1234567890", builder.ToString()); + } + + [Fact] + public void Append_AfterTruncation_IgnoresFutureAppends() + { + using var builder = new TruncatingStringBuilder(); + + // Fill to 250 characters + builder.Append(new string('a', 250)); + + // Trigger truncation + builder.Append("123456"); + Assert.True(builder.IsTruncated); + Assert.Equal(250, builder.Length); + + // Try to append something that would fit in remaining space + builder.Append("x"); + + // Should still be ignored + Assert.Equal(250, builder.Length); + Assert.DoesNotContain("x", builder.ToString()); + } + + [Fact] + public void Append_ExactlyMaxLength_DoesNotTruncate() + { + using var builder = new TruncatingStringBuilder(); + builder.Append(new string('a', 255)); + + Assert.Equal(255, builder.Length); + Assert.False(builder.IsTruncated); + } + + [Fact] + public void Append_OneCharOverMaxLength_Truncates() + { + using var builder = new TruncatingStringBuilder(); + builder.Append(new string('a', 255)); + builder.Append('b'); + + Assert.Equal(255, builder.Length); + Assert.True(builder.IsTruncated); + Assert.DoesNotContain("b", builder.ToString()); + } + + [Fact] + public void TrimEnd_RemovesTrailingWhitespace() + { + using var builder = new TruncatingStringBuilder(); + builder.Append("hello "); + builder.TrimEnd(); + + Assert.Equal("hello", builder.ToString()); + Assert.Equal(5, builder.Length); + } + + [Fact] + public void TrimEnd_EmptyBuilder_DoesNotThrow() + { + using var builder = new TruncatingStringBuilder(); + builder.TrimEnd(); + + Assert.Equal(string.Empty, builder.ToString()); + Assert.Equal(0, builder.Length); + } + + [Fact] + public void Append_NullString_DoesNothing() + { + using var builder = new TruncatingStringBuilder(); + builder.Append(null!); + + Assert.Equal(string.Empty, builder.ToString()); + Assert.Equal(0, builder.Length); + } + + [Fact] + public void Append_EmptyString_DoesNothing() + { + using var builder = new TruncatingStringBuilder(); + builder.Append(string.Empty); + + Assert.Equal(string.Empty, builder.ToString()); + Assert.Equal(0, builder.Length); + } + + [Fact] + public void Dispose_CanBeCalledMultipleTimes() + { + var builder = new TruncatingStringBuilder(); + builder.Append("test"); + builder.Dispose(); + builder.Dispose(); // Should not throw + } + + [Fact] + public void ToString_AfterDispose_ReturnsEmpty() + { + var builder = new TruncatingStringBuilder(); + builder.Append("test"); + builder.Dispose(); + + Assert.Equal(string.Empty, builder.ToString()); + } +} From d071901f3039431321760fb24ba5c43f56b73cbf Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 24 Nov 2025 17:07:16 -0800 Subject: [PATCH 17/46] Perf: Simplify text edits --- .../Implementation/KustoProcessor.cs | 36 ++++--------------- .../README.md | 16 ++++----- 2 files changed, 15 insertions(+), 37 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index aba4b2f03d..f0661a1b39 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -42,21 +42,8 @@ private static string Sanitize(KustoCode code) var collector = new SanitizerVisitor(); code.Syntax.Accept(collector); - // Build edits - var edits = new List(); - foreach (var replacement in collector.Replacements) - { - var edit = replacement.Kind switch - { - ReplacementKind.Placeholder => TextEdit.Replacement(replacement.Element.TextStart, replacement.Element.Width, "?"), - ReplacementKind.Remove => TextEdit.Deletion(replacement.Element.TextStart, replacement.Element.Width), - _ => throw new NotSupportedException($"Unexpected replacement kind: {replacement.Kind}"), - }; - - edits.Add(edit); - } - // Apply edits to text + var edits = collector.Edits; var text = new EditString(code.Text); if (edits.Count == 0 || !text.CanApplyAll(edits)) { @@ -74,30 +61,21 @@ private static string Summarize(KustoCode code) return walker.GetSummary(); } - private readonly struct Replacement - { - public Replacement(SyntaxElement element, ReplacementKind kind) - { - this.Element = element; - this.Kind = kind; - } + private static TextEdit CreatePlaceholder(SyntaxElement node) => TextEdit.Replacement(node.TextStart, node.Width, "?"); - public SyntaxElement Element { get; } - - public ReplacementKind Kind { get; } - } + private static TextEdit CreateRemoval(SyntaxElement node) => TextEdit.Deletion(node.TextStart, node.Width); private sealed class SanitizerVisitor : DefaultSyntaxVisitor { - public readonly List Replacements = []; + public readonly List Edits = []; - public override void VisitLiteralExpression(LiteralExpression node) => this.Replacements.Add(new Replacement(node, ReplacementKind.Placeholder)); + public override void VisitLiteralExpression(LiteralExpression node) => this.Edits.Add(CreatePlaceholder(node)); - public override void VisitDynamicExpression(DynamicExpression node) => this.Replacements.Add(new Replacement(node, ReplacementKind.Placeholder)); + public override void VisitDynamicExpression(DynamicExpression node) => this.Edits.Add(CreatePlaceholder(node)); public override void VisitPrefixUnaryExpression(PrefixUnaryExpression node) { - this.Replacements.Add(new Replacement(node.Operator, ReplacementKind.Remove)); + this.Edits.Add(CreateRemoval(node.Operator)); base.VisitPrefixUnaryExpression(node); } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 249119d108..31a7ebd673 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -22,16 +22,16 @@ option number from the list of options shown on the Console window. ``` -BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.7092/24H2/2024Update/HudsonValley) -Intel Core i9-10940X CPU 3.30GHz (Max: 3.31GHz), 1 CPU, 28 logical and 14 physical cores -.NET SDK 10.0.100 - [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 - DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7093) +Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores +.NET SDK 10.0.100-rc.2.25502.107 + [Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3 ``` | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |---------------------------- |---------:|---------:|---------:|-------:|-------:|----------:| -| ProcessSummarizeAndSanitize | 25.24 μs | 0.490 μs | 0.459 μs | 2.8381 | - | 27.94 KB | -| ProcessSummarizeOnly | 22.30 μs | 0.436 μs | 0.408 μs | 2.7161 | 0.2136 | 26.81 KB | -| ProcessSanitizeOnly | 23.91 μs | 0.417 μs | 0.528 μs | 2.8076 | 0.1526 | 27.73 KB | +| ProcessSummarizeAndSanitize | 14.98 μs | 0.333 μs | 0.954 μs | 1.2512 | 0.0305 | 15.55 KB | +| ProcessSummarizeOnly | 11.24 μs | 0.271 μs | 0.781 μs | 1.1749 | 0.0305 | 14.49 KB | +| ProcessSanitizeOnly | 11.25 μs | 0.216 μs | 0.231 μs | 1.2512 | 0.0305 | 15.38 KB | From 12d16556f7a8445180adbbc8612171ab03ee27b0 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 26 Nov 2025 10:17:22 -0800 Subject: [PATCH 18/46] Perf: Reuse GlobalState --- .../Implementation/KustoProcessor.cs | 4 +++- .../README.md | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index f0661a1b39..acad62f969 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -10,6 +10,8 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal static class KustoProcessor { + private static readonly GlobalState KustoParserGlobalState = GlobalState.Default.WithCache(); + private enum ReplacementKind { Placeholder, @@ -18,7 +20,7 @@ private enum ReplacementKind public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, string query) { - var code = KustoCode.ParseAndAnalyze(query); + var code = KustoCode.ParseAndAnalyze(query, KustoParserGlobalState); string? summarized = null; string? sanitized = null; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 31a7ebd673..e87a694a73 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -30,8 +30,8 @@ Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|---------------------------- |---------:|---------:|---------:|-------:|-------:|----------:| -| ProcessSummarizeAndSanitize | 14.98 μs | 0.333 μs | 0.954 μs | 1.2512 | 0.0305 | 15.55 KB | -| ProcessSummarizeOnly | 11.24 μs | 0.271 μs | 0.781 μs | 1.1749 | 0.0305 | 14.49 KB | -| ProcessSanitizeOnly | 11.25 μs | 0.216 μs | 0.231 μs | 1.2512 | 0.0305 | 15.38 KB | +| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | +|---------------------------- |----------:|----------:|----------:|----------:|-------:|-------:|----------:| +| ProcessSummarizeAndSanitize | 11.976 μs | 0.3156 μs | 0.8952 μs | 11.705 μs | 1.0681 | 0.0153 | 13.11 KB | +| ProcessSummarizeOnly | 9.203 μs | 0.1820 μs | 0.4429 μs | 9.125 μs | 0.9918 | 0.0153 | 12.23 KB | +| ProcessSanitizeOnly | 9.388 μs | 0.1870 μs | 0.4992 μs | 9.325 μs | 1.0529 | 0.0153 | 12.95 KB | From aa68fcf420b58c223b306426fe5a0d275b51c9a9 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 26 Nov 2025 15:45:23 -0800 Subject: [PATCH 19/46] Add option to disable summarization Add an option to disable query summarization --- .../.publicApi/PublicAPI.Unshipped.txt | 2 ++ .../Implementation/KustoProcessor.cs | 9 ++++++--- .../Implementation/KustoTraceListener.cs | 3 +-- .../KustoInstrumentationOptions.cs | 6 ++++++ .../KustoProcessorBenchmarks.cs | 3 +++ .../README.md | 11 +++++----- .../KustoIntegrationTests.cs | 20 +++++++++++++------ ...- take 10_processQuery=False.verified.txt} | 5 +---- ... - take 10_processQuery=True.verified.txt} | 0 ...number=42_processQuery=False.verified.txt} | 7 ++----- ... number=42_processQuery=True.verified.txt} | 0 11 files changed, 41 insertions(+), 25 deletions(-) rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt => KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt} (90%) rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt => KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt} (100%) rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt => KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt} (91%) rename test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/{KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt => KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt} (100%) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 7d716fdc98..85e22127b4 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.get -> bool +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.set -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.get -> bool OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.set -> void OpenTelemetry.Metrics.MeterProviderBuilderExtensions diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index acad62f969..83a5569891 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -18,20 +18,23 @@ private enum ReplacementKind Remove, } - public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, string query) + public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, ReadOnlySpan query) { - var code = KustoCode.ParseAndAnalyze(query, KustoParserGlobalState); - string? summarized = null; string? sanitized = null; + KustoCode? code = null; + + // Note that order matters here as summarization requires semantic analysis, but we want to avoid parsing twice if both are requested. if (shouldSummarize) { + code ??= KustoCode.ParseAndAnalyze(query.ToString(), KustoParserGlobalState); summarized = Summarize(code); } if (shouldSanitize) { + code ??= KustoCode.Parse(query.ToString(), KustoParserGlobalState); sanitized = Sanitize(code); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index b6ed714fbf..0ed4d190f2 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -131,8 +131,7 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) if (!text.IsEmpty) { - var queryText = text.ToString(); - var info = KustoProcessor.Process(shouldSummarize: true, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, queryText); + var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, text); // Set sanitized query text if configured if (KustoInstrumentation.TracingOptions.RecordQueryText) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index 2d83790c9b..f1b2b2745d 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -14,5 +14,11 @@ public class KustoInstrumentationOptions /// public bool RecordQueryText { get; set; } + /// + /// Gets or sets a value indicating whether a summary of the query should be recorded as an attribute on the activity. + /// Default is . + /// + public bool RecordQuerySummary { get; set; } = true; + // TODO: Add flag for query parameter tracing } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorBenchmarks.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorBenchmarks.cs index b10e635c9e..d42c77c226 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorBenchmarks.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/KustoProcessorBenchmarks.cs @@ -20,4 +20,7 @@ public class KustoProcessorBenchmarks [Benchmark] public void ProcessSanitizeOnly() => KustoProcessor.Process(shouldSummarize: false, shouldSanitize: true, this.Query); + + [Benchmark] + public void ProcessNeither() => KustoProcessor.Process(shouldSummarize: false, shouldSanitize: false, this.Query); } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index e87a694a73..581cea77dd 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -30,8 +30,9 @@ Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores ``` -| Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated | -|---------------------------- |----------:|----------:|----------:|----------:|-------:|-------:|----------:| -| ProcessSummarizeAndSanitize | 11.976 μs | 0.3156 μs | 0.8952 μs | 11.705 μs | 1.0681 | 0.0153 | 13.11 KB | -| ProcessSummarizeOnly | 9.203 μs | 0.1820 μs | 0.4429 μs | 9.125 μs | 0.9918 | 0.0153 | 12.23 KB | -| ProcessSanitizeOnly | 9.388 μs | 0.1870 μs | 0.4992 μs | 9.325 μs | 1.0529 | 0.0153 | 12.95 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|---------------------------- |----------:|----------:|----------:|-------:|-------:|----------:| +| ProcessSummarizeAndSanitize | 10.141 μs | 0.1926 μs | 0.2436 μs | 1.0834 | 0.0153 | 13.36 KB | +| ProcessSummarizeOnly | 9.549 μs | 0.1772 μs | 0.1571 μs | 1.0071 | 0.0153 | 12.48 KB | +| ProcessSanitizeOnly | 4.154 μs | 0.0827 μs | 0.1832 μs | 0.5798 | 0.0038 | 7.13 KB | +| ProcessNeither | 0.0566 ns | 0.0259 ns | 0.0610 ns | - | - | - | diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index a0a5f1eb3f..e996abf502 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -27,14 +27,18 @@ public KustoIntegrationTests(KustoIntegrationTestsFixture fixture) [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] [InlineData("print number=42", true)] [InlineData("print number=42", false)] - public async Task SuccessfulQueryTest(string query, bool recordQueryText) + public async Task SuccessfulQueryTest(string query, bool processQuery) { var activities = new List(); var metrics = new List(); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInMemoryExporter(activities) - .AddKustoInstrumentation(options => options.RecordQueryText = recordQueryText) + .AddKustoInstrumentation(options => + { + options.RecordQueryText = processQuery; + options.RecordQuerySummary = processQuery; + }) .Build(); using var meterProvider = Sdk.CreateMeterProviderBuilder() @@ -90,20 +94,24 @@ await Verify( .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) .UseDirectory("Snapshots") - .UseParameters(query, recordQueryText); + .UseParameters(query, processQuery); } [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] [InlineData("InvalidTable | take 10", true)] [InlineData("InvalidTable | take 10", false)] - public async Task FailedQueryTest(string query, bool recordQueryText) + public async Task FailedQueryTest(string query, bool processQuery) { var activities = new List(); var metrics = new List(); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInMemoryExporter(activities) - .AddKustoInstrumentation(options => options.RecordQueryText = recordQueryText) + .AddKustoInstrumentation(options => + { + options.RecordQueryText = processQuery; + options.RecordQuerySummary = processQuery; + }) .Build(); using var meterProvider = Sdk.CreateMeterProviderBuilder() @@ -172,7 +180,7 @@ await Verify( .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) .UseDirectory("Snapshots") - .UseParameters(query, recordQueryText); + .UseParameters(query, processQuery); } [EnabledOnDockerPlatformFact(DockerPlatform.Linux)] diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt similarity index 90% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index 705a5921b5..da6cd61828 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: InvalidTable | take, + DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', @@ -20,9 +20,6 @@ }, { server.address: {Hostname}:{Port} - }, - { - db.query.summary: InvalidTable | take } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt similarity index 100% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_recordQueryText=True.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt similarity index 91% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index 6eb7273a36..fd3806da45 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -1,7 +1,7 @@ { Activities: [ { - DisplayName: print, + DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, Status: Ok, Tags: [ @@ -19,9 +19,6 @@ }, { server.address: {Hostname}:{Port} - }, - { - db.query.summary: print } ], OperationName: KD.RestClient.ExecuteQuery, @@ -42,4 +39,4 @@ Temporality: Cumulative } ] -} +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt similarity index 100% rename from test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_recordQueryText=True.verified.txt rename to test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt From aa1555c428bdc07fbdee55139b9659be18c64703 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Wed, 26 Nov 2025 23:32:36 -0800 Subject: [PATCH 20/46] Do not sanitize parameterized queries --- .../Implementation/KustoProcessor.cs | 26 ++++++++++++++++--- .../KustoQueryParserTests.cs | 7 +++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index 83a5569891..d45ce9380e 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -47,6 +47,11 @@ private static string Sanitize(KustoCode code) var collector = new SanitizerVisitor(); code.Syntax.Accept(collector); + if (!collector.ShouldSanitize) + { + return code.Text; + } + // Apply edits to text var edits = collector.Edits; var text = new EditString(code.Text); @@ -72,18 +77,31 @@ private static string Summarize(KustoCode code) private sealed class SanitizerVisitor : DefaultSyntaxVisitor { - public readonly List Edits = []; + private readonly List edits = []; - public override void VisitLiteralExpression(LiteralExpression node) => this.Edits.Add(CreatePlaceholder(node)); + public IReadOnlyList Edits => this.edits; - public override void VisitDynamicExpression(DynamicExpression node) => this.Edits.Add(CreatePlaceholder(node)); + /// + /// Gets a value indicating whether the query should be sanitized. + /// + /// + /// If the query is parameterized, we should skip sanitization. + /// https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext. + /// + public bool ShouldSanitize { get; private set; } = true; + + public override void VisitLiteralExpression(LiteralExpression node) => this.edits.Add(CreatePlaceholder(node)); + + public override void VisitDynamicExpression(DynamicExpression node) => this.edits.Add(CreatePlaceholder(node)); public override void VisitPrefixUnaryExpression(PrefixUnaryExpression node) { - this.Edits.Add(CreateRemoval(node.Operator)); + this.edits.Add(CreateRemoval(node.Operator)); base.VisitPrefixUnaryExpression(node); } + public override void VisitQueryParametersStatement(QueryParametersStatement node) => this.ShouldSanitize = false; + protected override void DefaultVisit(SyntaxNode node) => this.VisitChildren(node); private void VisitChildren(SyntaxNode node) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs index 0063aee8b7..333c760976 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoQueryParserTests.cs @@ -94,6 +94,13 @@ public class KustoQueryParserTests "let x = ?; let y = ?; StormEvents | take x" }, + // Parameterized queries + { + "declare query_parameters(maxInjured:long = 90);\nStormEvents\n| where InjuriesDirect + InjuriesIndirect > maxInjured\n| where EventType = 1\n| project EpisodeId, EventType, totalInjuries = InjuriesDirect + InjuriesIndirect", + "StormEvents | where | where | project", + "declare query_parameters(maxInjured:long = 90);\nStormEvents\n| where InjuriesDirect + InjuriesIndirect > maxInjured\n| where EventType = 1\n| project EpisodeId, EventType, totalInjuries = InjuriesDirect + InjuriesIndirect" + }, + // Nested queries { "StormEvents | union OtherEvents", From 4f09e1fcebe8b3444dfb347bbb1630b820d9406b Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 30 Nov 2025 19:09:07 -0800 Subject: [PATCH 21/46] Extrace TraceRecord parsing to own class and add tests --- .../Implementation/KustoProcessor.cs | 6 +- .../Implementation/KustoTraceListener.cs | 66 ++------- .../Implementation/TraceRecordParser.cs | 112 +++++++++++++++ .../TraceRecordParserTests.cs | 127 ++++++++++++++++++ 4 files changed, 252 insertions(+), 59 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index d45ce9380e..7e7f67aa91 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -18,7 +18,7 @@ private enum ReplacementKind Remove, } - public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, ReadOnlySpan query) + public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, string query) { string? summarized = null; string? sanitized = null; @@ -28,13 +28,13 @@ public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSaniti // Note that order matters here as summarization requires semantic analysis, but we want to avoid parsing twice if both are requested. if (shouldSummarize) { - code ??= KustoCode.ParseAndAnalyze(query.ToString(), KustoParserGlobalState); + code ??= KustoCode.ParseAndAnalyze(query, KustoParserGlobalState); summarized = Summarize(code); } if (shouldSanitize) { - code ??= KustoCode.Parse(query.ToString(), KustoParserGlobalState); + code ??= KustoCode.Parse(query, KustoParserGlobalState); sanitized = Sanitize(code); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 0ed4d190f2..997b83689b 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -12,10 +12,6 @@ internal sealed class KustoTraceListener : KustoUtils.ITraceListener { private readonly ConcurrentDictionary activities = new(); - public KustoTraceListener() - { - } - public override string Name => nameof(KustoTraceListener); public override bool IsThreadSafe => true; @@ -50,48 +46,11 @@ public override void Write(KustoUtils.TraceRecord record) } } - private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan source, ReadOnlySpan start, ReadOnlySpan end) - { - var startIndex = source.IndexOf(start); - if (startIndex < 0) - { - return ReadOnlySpan.Empty; - } - - startIndex += start.Length; - var remaining = source.Slice(startIndex); - - var endIndex = remaining.IndexOf(end); - if (endIndex < 0) - { - endIndex = remaining.Length; - } - - return remaining.Slice(0, endIndex); - } - - private static string GetServerAddress(ReadOnlySpan uri) - { - var schemeEnd = uri.IndexOf("://".AsSpan()); - if (schemeEnd < 0) - { - return string.Empty; - } - - var hostStart = schemeEnd + 3; - var remaining = uri.Slice(hostStart); - - var pathStart = remaining.IndexOf('/'); - var host = pathStart >= 0 ? remaining.Slice(0, pathStart) : remaining; - - return host.ToString(); - } - private void HandleException(KustoUtils.TraceRecord record) { var activity = this.GetActivity(record); - var message = ExtractValueBetween(record.Message.AsSpan(), "ErrorMessage=".AsSpan(), Environment.NewLine.AsSpan()); - activity?.SetStatus(ActivityStatusCode.Error, message.ToString()); + var result = TraceRecordParser.ParseException(record.Message.AsSpan()); + activity?.SetStatus(ActivityStatusCode.Error, result.ErrorMessage.ToString()); } private void HandleHttpRequestStart(KustoUtils.TraceRecord record) @@ -110,14 +69,12 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) activity.SetTag(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); - var message = record.Message.AsSpan(); + var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); - var uri = ExtractValueBetween(message, "Uri=".AsSpan(), ",".AsSpan()); - if (!uri.IsEmpty) + if (!result.Uri.IsEmpty) { - var uriString = uri.ToString(); - activity.SetTag(SemanticConventions.AttributeUrlFull, uriString); - activity.SetTag(SemanticConventions.AttributeServerAddress, GetServerAddress(uri)); + activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri.ToString()); + activity.SetTag(SemanticConventions.AttributeServerAddress, result.Host.ToString()); string? database = null; // TODO: Add parsing for database when available if (!string.IsNullOrEmpty(database)) @@ -126,12 +83,9 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) } } - // Extract and parse query text - var text = ExtractValueBetween(message, "text=".AsSpan(), Environment.NewLine.AsSpan()); - - if (!text.IsEmpty) + if (!result.QueryText.IsEmpty) { - var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, text); + var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, result.QueryText.ToString()); // Set sanitized query text if configured if (KustoInstrumentation.TracingOptions.RecordQueryText) @@ -172,8 +126,8 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) { - var howEnded = ExtractValueBetween(record.Message.AsSpan(), "HowEnded=".AsSpan(), ",".AsSpan()); - if (howEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) + var result = TraceRecordParser.ParseActivityComplete(record.Message.AsSpan()); + if (result.HowEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) { activity.SetStatus(ActivityStatusCode.Ok); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs new file mode 100644 index 0000000000..a6d81dc100 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -0,0 +1,112 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET9_0_OR_GREATER +using System.Buffers; +#endif + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal class TraceRecordParser +{ +#if NET9_0_OR_GREATER + private static readonly SearchValues Delimiters = SearchValues.Create([',', '\n']); +#else + private static readonly char[] Delimiters = [',', '\n']; +#endif + + public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) + { + var uri = ExtractValueBetween(message, "Uri="); + var host = GetServerAddress(uri); + var queryText = ExtractValueBetween(message, "text="); + + return new ParsedRequestStart(uri, host, queryText); + } + + public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan message) + { + var howEnded = ExtractValueBetween(message, "HowEnded="); + return new ParsedActivityComplete(howEnded); + } + + public static ParsedException ParseException(ReadOnlySpan message) + { + var errorMessage = ExtractValueBetween(message, "ErrorMessage="); + return new ParsedException(errorMessage); + } + + private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystack, ReadOnlySpan needle) + { + var startIndex = haystack.IndexOf(needle); + if (startIndex < 0) + { + return ReadOnlySpan.Empty; + } + + startIndex += needle.Length; + var remaining = haystack.Slice(startIndex); + + var endIndex = remaining.IndexOfAny(Delimiters); + if (endIndex < 0) + { + endIndex = remaining.Length; + } + + var result = remaining.Slice(0, endIndex); + result = result.Trim(); // Trim to specifically handle newlines, which may be multiple characters + + return result; + } + + private static ReadOnlySpan GetServerAddress(ReadOnlySpan uri) + { + var schemeEnd = uri.IndexOf("://".AsSpan()); + if (schemeEnd < 0) + { + return ReadOnlySpan.Empty; + } + + var hostStart = schemeEnd + 3; + var remaining = uri.Slice(hostStart); + + var pathStart = remaining.IndexOf('/'); + var host = pathStart >= 0 ? remaining.Slice(0, pathStart) : remaining; + + return host; + } + + internal readonly ref struct ParsedRequestStart + { + public readonly ReadOnlySpan Uri; + public readonly ReadOnlySpan Host; + public readonly ReadOnlySpan QueryText; + + public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan host, ReadOnlySpan queryText) + { + this.Uri = uri; + this.Host = host; + this.QueryText = queryText; + } + } + + internal readonly ref struct ParsedActivityComplete + { + public readonly ReadOnlySpan HowEnded; + + public ParsedActivityComplete(ReadOnlySpan howEnded) + { + this.HowEnded = howEnded; + } + } + + internal readonly ref struct ParsedException + { + public readonly ReadOnlySpan ErrorMessage; + + public ParsedException(ReadOnlySpan errorMessage) + { + this.ErrorMessage = errorMessage; + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs new file mode 100644 index 0000000000..63776ecf3d --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -0,0 +1,127 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class TraceRecordParserTests +{ + [Fact] + public void ParseRequestStart() + { + const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7"; + var result = TraceRecordParser.ParseRequestStart(message); + + Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri.ToString()); + Assert.Equal("InvalidTable | take 10 | where Col1=7", result.QueryText.ToString()); + } + + [Fact] + public void ParseActivityComplete() + { + const string message = "MonitoredActivityCompletedSuccessfully: ActivityType=KD.RestClient.ExecuteQuery, Timestamp=2025-12-01T02:30:30.0211167Z, ParentActivityId=52707aa6-de7f-42dd-adb9-bc3e6d976fa6, Duration=4316.802 [ms], HowEnded=Success"; + var result = TraceRecordParser.ParseActivityComplete(message); + + Assert.Equal("Success", result.HowEnded.ToString()); + } + + [Fact] + public void ParseException() + { + const string message = + """ + Exception object created: Kusto.Data.Exceptions.SemanticException + [0]Kusto.Data.Exceptions.SemanticException: Semantic error: 'take' operator: Failed to resolve table or column expression named 'InvalidTable' + Timestamp=2025-12-01T02:39:36.3878585Z + ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + ActivityId=b329e166-812e-40e5-9589-5667b8e1329d + ActivityType=KD.RestClient.ExecuteQuery + MachineName=MATTKOT-SURFACE + ProcessName=testhost + ProcessId=44216 + ThreadId=29176 + ActivityStack=(Activity stack: CRID=SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== ARID=b329e166-812e-40e5-9589-5667b8e1329d > KD.RestClient.ExecuteQuery/b329e166-812e-40e5-9589-5667b8e1329d) + MonitoredActivityContext=(ActivityType=KD.RestClient.ExecuteQuery, Timestamp=2025-12-01T02:39:36.1683275Z, ParentActivityId=b329e166-812e-40e5-9589-5667b8e1329d, TimeSinceStarted=219.5397 [ms])ErrorCode=SEM0100 + ErrorReason=BadRequest + ErrorMessage='take' operator: Failed to resolve table or column expression named 'InvalidTable' + DataSource=http://127.0.0.1:62413/v1/rest/query + DatabaseName=NetDefaultDB + ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + ActivityId=ee26fe2b-ae7d-4f9c-807c-117bcae21338 + SemanticErrors='take' operator: Failed to resolve table or column expression named 'InvalidTable' + + at Kusto.Cloud.Platform.Utils.ExceptionsTemplateHelper.Construct_Trace(Exception that, ITraceSource traceSource) + at Kusto.Data.Exceptions.SemanticException.Construct_Trace() + at Kusto.Data.Exceptions.SemanticException.Construct(Boolean deserializing, Nullable`1 failureCode, String failureSubCode, Nullable`1 isPermanent) + at Kusto.Data.Exceptions.SemanticException..ctor(String text, String semanticErrors, String errorCode, String errorReason, String errorMessage, String dataSource, String databaseName, String clientRequestId, Guid activityId, Nullable`1 failureCode, String failureSubCode, Nullable`1 isPermanent) + at Kusto.Data.Net.KustoExceptionUtils.ToKustoException(String responseBody, HttpStatusCode statusCode, String reasonPhrase, KustoExceptionContext context, ITraceSource tracer) + at Kusto.Data.Net.Client.KustoDataHttpClient.ThrowKustoExceptionFromResponseMessageAsync(KustoProtocolResponse response, KustoExceptionContext exceptionContext, HttpResponseMessage responseMessage, ClientRequestProperties properties, Boolean shouldBuffer, Action`2 notify) + at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine) + at Kusto.Data.Net.Client.KustoDataHttpClient.ThrowKustoExceptionFromResponseMessageAsync(KustoProtocolResponse response, KustoExceptionContext exceptionContext, HttpResponseMessage responseMessage, ClientRequestProperties properties, Boolean shouldBuffer, Action`2 notify) + at Kusto.Data.Net.Client.RestClient2.MakeHttpRequestAsyncImpl(RestApi restApi, String address, String csl, String ns, String databaseName, Boolean streaming, ClientRequestProperties properties, ServiceModelTimeoutKind timeoutKind, String clientRequestId, Stream body, StreamProperties streamProperties, CancellationToken cancellationToken, KustoProtocolRequest request, String hostHeaderOverride, HttpMethod httpMethod) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Threading.Tasks.Task`1.TrySetResult(TResult result) + at System.Threading.Tasks.Task.TwoTaskWhenAnyPromise`1.Invoke(Task completingTask) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetResult(TResult result) + at System.Net.Http.SocketsHttpHandler.g__CreateHandlerAndSendAsync|115_0(HttpRequestMessage request, CancellationToken cancellationToken) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(Task`1 task, TResult result) + at System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(Task`1 task, TResult result) + at System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, Boolean async, Boolean doRequestAuth, CancellationToken cancellationToken) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetResult(TResult result) + at System.Net.Http.HttpConnection.SendAsync(HttpRequestMessage request, Boolean async, CancellationToken cancellationToken) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Threading.Tasks.AwaitTaskContinuation.RunOrScheduleAction(Action action, Boolean allowInlining) + at System.Threading.Tasks.Task.RunContinuations(Object continuationObject) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.SetExistingTaskResult(Task`1 task, TResult result) + at System.Net.Http.HttpConnection.InitialFillAsync(Boolean async) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.ExecutionContextCallback(Object s) + at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext(Thread threadPoolThread) + at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1.AsyncStateMachineBox`1.MoveNext() + at System.Net.Sockets.SocketAsyncEventArgs.<>c.<.cctor>b__174_0(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped) + at System.Threading.ThreadPoolTypedWorkItemQueue.System.Threading.IThreadPoolWorkItem.Execute() + at System.Threading.ThreadPoolWorkQueue.Dispatch() + at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart() + at System.Threading.Thread.StartCallback() + """; + + var result = TraceRecordParser.ParseException(message); + + Assert.Equal("'take' operator: Failed to resolve table or column expression named 'InvalidTable'", result.ErrorMessage.ToString()); + } +} From 9c3ef2f995b5a4b8bdb45c9cb1fa867b3b7156c0 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 30 Nov 2025 21:47:12 -0800 Subject: [PATCH 22/46] Handle embedded delimiters in query text --- .../Implementation/Polyfills.cs | 19 ------------ .../Implementation/SpanExtensions.cs | 19 ++++++++++++ .../Implementation/TraceRecordParser.cs | 29 +++++-------------- .../TraceRecordParserTests.cs | 4 +-- 4 files changed, 29 insertions(+), 42 deletions(-) delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs deleted file mode 100644 index 88e5974ca3..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/Polyfills.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -internal static class Polyfills -{ -#if NETFRAMEWORK || NETSTANDARD - public static bool StartsWith(this string target, char value) - { - if (target.Length == 0) - { - return false; - } - - return target[0] == value; - } -#endif -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs new file mode 100644 index 0000000000..dd0fdc31b7 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class SpanExtensions +{ + public static ReadOnlySpan SliceAfter(this ReadOnlySpan span, ReadOnlySpan needle) + { + var idx = span.IndexOf(needle); + return idx >= 0 ? span.Slice(idx + needle.Length) : []; + } + + public static ReadOnlySpan SliceBefore(this ReadOnlySpan span, ReadOnlySpan needle) + { + var idx = span.IndexOf(needle); + return idx >= 0 ? span.Slice(0, idx) : []; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index a6d81dc100..58e07dfcf4 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -19,7 +19,10 @@ public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) { var uri = ExtractValueBetween(message, "Uri="); var host = GetServerAddress(uri); - var queryText = ExtractValueBetween(message, "text="); + + // Query text may have embedded delimiters, however it is always the last field in the message + // so we can just take everything after "text=" + var queryText = message.SliceAfter("text="); return new ParsedRequestStart(uri, host, queryText); } @@ -38,14 +41,7 @@ public static ParsedException ParseException(ReadOnlySpan message) private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystack, ReadOnlySpan needle) { - var startIndex = haystack.IndexOf(needle); - if (startIndex < 0) - { - return ReadOnlySpan.Empty; - } - - startIndex += needle.Length; - var remaining = haystack.Slice(startIndex); + var remaining = haystack.SliceAfter(needle); var endIndex = remaining.IndexOfAny(Delimiters); if (endIndex < 0) @@ -61,19 +57,10 @@ private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystac private static ReadOnlySpan GetServerAddress(ReadOnlySpan uri) { - var schemeEnd = uri.IndexOf("://".AsSpan()); - if (schemeEnd < 0) - { - return ReadOnlySpan.Empty; - } - - var hostStart = schemeEnd + 3; - var remaining = uri.Slice(hostStart); + var result = uri.SliceAfter("://"); + result = result.SliceBefore(['/']); - var pathStart = remaining.IndexOf('/'); - var host = pathStart >= 0 ? remaining.Slice(0, pathStart) : remaining; - - return host; + return result; } internal readonly ref struct ParsedRequestStart diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 63776ecf3d..25e68d1fbb 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -11,11 +11,11 @@ public class TraceRecordParserTests [Fact] public void ParseRequestStart() { - const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7"; + const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; var result = TraceRecordParser.ParseRequestStart(message); Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri.ToString()); - Assert.Equal("InvalidTable | take 10 | where Col1=7", result.QueryText.ToString()); + Assert.Equal("InvalidTable | take 10 | where Col1=7 | summarize by Date, Time", result.QueryText.ToString()); } [Fact] From cecc02c6631167fcf467097692a12afcf3320d74 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 30 Nov 2025 21:56:04 -0800 Subject: [PATCH 23/46] Reduce depedency to Kusto.Cloud.Platform and bump version --- Directory.Packages.props | 3 ++- .../OpenTelemetry.Instrumentation.Kusto.csproj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0290442c5f..feaba01c3d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -83,7 +83,7 @@ - + @@ -119,6 +119,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index 89bdd11932..a448928015 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -22,7 +22,7 @@ - + From 020acb3ec9e08449c0d4b12978da9bf07ff9b728 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 30 Nov 2025 22:06:25 -0800 Subject: [PATCH 24/46] Add database to attributes --- .../Implementation/KustoTraceListener.cs | 13 ++++++++----- .../Implementation/TraceRecordParser.cs | 7 +++++-- ...dTable - take 10_processQuery=False.verified.txt | 3 +++ ...idTable - take 10_processQuery=True.verified.txt | 3 +++ ...=print number=42_processQuery=False.verified.txt | 3 +++ ...y=print number=42_processQuery=True.verified.txt | 3 +++ .../TraceRecordParserTests.cs | 4 +++- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 997b83689b..f883142b65 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -74,13 +74,16 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) if (!result.Uri.IsEmpty) { activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri.ToString()); + } + + if (!result.Host.IsEmpty) + { activity.SetTag(SemanticConventions.AttributeServerAddress, result.Host.ToString()); + } - string? database = null; // TODO: Add parsing for database when available - if (!string.IsNullOrEmpty(database)) - { - activity.SetTag(SemanticConventions.AttributeDbNamespace, database); - } + if (!result.Database.IsEmpty) + { + activity.SetTag(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); } if (!result.QueryText.IsEmpty) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index 58e07dfcf4..e94c636969 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -19,12 +19,13 @@ public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) { var uri = ExtractValueBetween(message, "Uri="); var host = GetServerAddress(uri); + var database = ExtractValueBetween(message, "DatabaseName="); // Query text may have embedded delimiters, however it is always the last field in the message // so we can just take everything after "text=" var queryText = message.SliceAfter("text="); - return new ParsedRequestStart(uri, host, queryText); + return new ParsedRequestStart(uri, host, database, queryText); } public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan message) @@ -67,12 +68,14 @@ internal readonly ref struct ParsedRequestStart { public readonly ReadOnlySpan Uri; public readonly ReadOnlySpan Host; + public readonly ReadOnlySpan Database; public readonly ReadOnlySpan QueryText; - public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan host, ReadOnlySpan queryText) + public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan host, ReadOnlySpan database, ReadOnlySpan queryText) { this.Uri = uri; this.Host = host; + this.Database = database; this.QueryText = queryText; } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index da6cd61828..c695a15334 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -20,6 +20,9 @@ }, { server.address: {Hostname}:{Port} + }, + { + db.namespace: NetDefaultDB } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 3369ebc297..ce38cbebc6 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -21,6 +21,9 @@ { server.address: {Hostname}:{Port} }, + { + db.namespace: NetDefaultDB + }, { db.query.text: InvalidTable | take ? }, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index fd3806da45..ceb644d456 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -19,6 +19,9 @@ }, { server.address: {Hostname}:{Port} + }, + { + db.namespace: NetDefaultDB } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt index 95e050e7f2..e4c143b943 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt @@ -20,6 +20,9 @@ { server.address: {Hostname}:{Port} }, + { + db.namespace: NetDefaultDB + }, { db.query.text: print number=? }, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 25e68d1fbb..70333e1fa6 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -11,10 +11,12 @@ public class TraceRecordParserTests [Fact] public void ParseRequestStart() { - const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; + const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; var result = TraceRecordParser.ParseRequestStart(message); Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri.ToString()); + Assert.Equal("127.0.0.1:49902", result.Host.ToString()); + Assert.Equal("NetDefaultDB", result.Database.ToString()); Assert.Equal("InvalidTable | take 10 | where Col1=7 | summarize by Date, Time", result.QueryText.ToString()); } From e5b9a4ebabfc23f0c047ecd77f6286269b592c31 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Sun, 30 Nov 2025 22:12:37 -0800 Subject: [PATCH 25/46] Add test coverage for TraceRecordParser --- .../TraceRecordParserTests.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 70333e1fa6..d847f0b896 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -9,7 +9,7 @@ namespace OpenTelemetry.Instrumentation.Kusto.Tests; public class TraceRecordParserTests { [Fact] - public void ParseRequestStart() + public void ParseRequestStartSuccess() { const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; var result = TraceRecordParser.ParseRequestStart(message); @@ -20,6 +20,18 @@ public void ParseRequestStart() Assert.Equal("InvalidTable | take 10 | where Col1=7 | summarize by Date, Time", result.QueryText.ToString()); } + [Fact] + public void ParseRequestStartFailure() + { + const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://"; + var result = TraceRecordParser.ParseRequestStart(message); + + Assert.Equal("http://", result.Uri.ToString()); + Assert.Equal(string.Empty, result.Host.ToString()); + Assert.Equal(string.Empty, result.Database.ToString()); + Assert.Equal(string.Empty, result.QueryText.ToString()); + } + [Fact] public void ParseActivityComplete() { @@ -29,6 +41,15 @@ public void ParseActivityComplete() Assert.Equal("Success", result.HowEnded.ToString()); } + [Fact] + public void ParseActivityCompleteFailure() + { + const string message = "MonitoredActivityCompletedSuccessfully: ActivityType=KD.RestClient.ExecuteQuery, Timestamp=2025-12-01T02:30:30.0211167Z, ParentActivityId=52707aa6-de7f-42dd-adb9-bc3e6d976fa6, Duration=4316.802 [ms]"; + var result = TraceRecordParser.ParseActivityComplete(message); + + Assert.Equal(string.Empty, result.HowEnded.ToString()); + } + [Fact] public void ParseException() { @@ -126,4 +147,13 @@ at System.Threading.Thread.StartCallback() Assert.Equal("'take' operator: Failed to resolve table or column expression named 'InvalidTable'", result.ErrorMessage.ToString()); } + + [Fact] + public void ParseExceptionFailure() + { + const string message = "Exception object created: Kusto.Data.Exceptions.SemanticException Timestamp=2025-12-01T02:39:36.3878585Z"; + var result = TraceRecordParser.ParseException(message); + + Assert.Equal(string.Empty, result.ErrorMessage.ToString()); + } } From 6012e7a75488254dc184b88237dd2335649e870a Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 1 Dec 2025 11:35:39 -0800 Subject: [PATCH 26/46] Add enrichment callback --- .../.publicApi/PublicAPI.Unshipped.txt | 2 + .../KustoInstrumentationEventSource.cs | 31 ++++++++ .../Implementation/KustoMetricListener.cs | 6 +- .../Implementation/KustoTraceListener.cs | 10 ++- .../KustoInstrumentationOptions.cs | 8 ++ ...OpenTelemetry.Instrumentation.Kusto.csproj | 1 + .../KustoIntegrationTests.cs | 76 +++++++++++++++++++ 7 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 85e22127b4..4af47d3f0c 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -1,5 +1,7 @@ #nullable enable OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.Enrich.get -> System.Action? +OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.Enrich.set -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.get -> bool OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.set -> void diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs new file mode 100644 index 0000000000..12e51577af --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs @@ -0,0 +1,31 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +/// +/// EventSource for Kusto instrumentation. +/// +[EventSource(Name = "OpenTelemetry-Instrumentation-Kusto")] +internal sealed class KustoInstrumentationEventSource : EventSource +{ + public static KustoInstrumentationEventSource Log { get; } = new(); + + [NonEvent] + public void EnrichmentException(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.EnrichmentException(ex.ToInvariantString()); + } + } + + [Event(1, Message = "Enrichment exception: {0}", Level = EventLevel.Error)] + public void EnrichmentException(string exception) + { + this.WriteEvent(1, exception); + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs index acaa8a78d9..464c4f6b08 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs @@ -9,11 +9,7 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal sealed class KustoMetricListener : KustoUtils.ITraceListener { - private AsyncLocal beginTimestamp = new(); - - public KustoMetricListener() - { - } + private readonly AsyncLocal beginTimestamp = new(); public override string Name => nameof(KustoMetricListener); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index f883142b65..ced6351167 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -90,7 +90,6 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) { var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, result.QueryText.ToString()); - // Set sanitized query text if configured if (KustoInstrumentation.TracingOptions.RecordQueryText) { activity.SetTag(SemanticConventions.AttributeDbQueryText, info.Sanitized); @@ -113,6 +112,15 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) // Fall back to operation name if no query text activity.DisplayName = operationName; } + + try + { + KustoInstrumentation.TracingOptions.Enrich?.Invoke(activity, record); + } + catch (Exception ex) + { + KustoInstrumentationEventSource.Log.EnrichmentException(ex); + } } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index f1b2b2745d..5260076ed3 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -1,6 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Diagnostics; +using KustoUtils = Kusto.Cloud.Platform.Utils; + namespace OpenTelemetry.Instrumentation.Kusto; /// @@ -20,5 +23,10 @@ public class KustoInstrumentationOptions /// public bool RecordQuerySummary { get; set; } = true; + /// + /// Gets or sets an action to enrich the Activity with additional information from the TraceRecord. + /// + public Action? Enrich { get; set; } + // TODO: Add flag for query parameter tracing } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index a448928015..dae6d0b5ca 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -31,6 +31,7 @@ + diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index e996abf502..807d363711 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -226,4 +226,80 @@ public void NoInstrumentationRegistered_NoEventsEmitted() Assert.Empty(kustoActivities); Assert.Empty(kustoMetrics); } + + [EnabledOnDockerPlatformFact(DockerPlatform.Linux)] + public void EnrichCallbackTest() + { + // Arrange + var activities = new List(); + + // Query with comment for custom summary + const string key = "otel-custom-summary="; + const string summary = "MyOperation"; + var query = $"// {key}{summary}\nprint number=42"; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(activities) + .AddKustoInstrumentation(options => + { + // Disable automatic summarization + options.RecordQuerySummary = false; + + // Extract the comment from the query text and set the summary attribute manually + options.Enrich = (activity, record) => + { + var message = record.Message.AsSpan(); + var begin = message.IndexOf(key, StringComparison.Ordinal); + + if (begin < 0) + { + return; + } + + var summary = message.Slice(begin + key.Length); + var end = summary.IndexOfAny('\r', '\n'); + if (end < 0) + { + end = summary.Length; + } + + summary = summary.Slice(0, end).Trim(); + var summaryString = summary.ToString(); + + activity.SetTag(SemanticConventions.AttributeDbQuerySummary, summaryString); + activity.DisplayName = summaryString; + }; + }) + .Build(); + + var kcsb = this.fixture.ConnectionStringBuilder; + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + ClientRequestId = "test-enrich-callback", + }; + + // Act + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + reader.Consume(); + + tracerProvider.ForceFlush(); + + // Assert + var kustoActivities = activities + .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) + .ToList(); + + Assert.Single(kustoActivities); + var activity = kustoActivities[0]; + + // Verify the custom summary was set by the Enrich callback + var querySummaryTag = activity.Tags.SingleOrDefault(t => t.Key == SemanticConventions.AttributeDbQuerySummary); + Assert.NotNull(querySummaryTag.Key); + Assert.Equal(summary, querySummaryTag.Value); + + // Verify the display name was set to the custom summary + Assert.Equal(summary, activity.DisplayName); + } } From 872b06d29a191579b4c2212fd848cc4002676e12 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 1 Dec 2025 11:43:56 -0800 Subject: [PATCH 27/46] Update readme to document options --- .../README.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index 9d9a82bbfd..c267b15253 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -75,6 +75,128 @@ public class Program } ``` +## Advanced configuration + +This instrumentation can be configured to change the default behavior by using +`KustoInstrumentationOptions`. + +### RecordQueryText + +This option can be set to instruct the instrumentation to record the sanitized +query text as an attribute on the activity. Query text is +sanitized to remove literal values and replace them with a placeholder character. + +The default value is `false` and can be changed by the code like below. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddKustoInstrumentation( + options => options.RecordQueryText = true) + .AddConsoleExporter() + .Build(); +``` + +### RecordQuerySummary + +This option can be set to instruct the instrumentation to record a query +summary as an attribute on the activity. The query summary +is automatically generated from the query text and contains the operation type +and relevant object names. + +The default value is `true` and can be changed by the code like below. + +```csharp +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddKustoInstrumentation( + options => options.RecordQuerySummary = false) + .AddConsoleExporter() + .Build(); +``` + +### Enrich + +This option can be used to enrich the activity with additional information from +the raw `TraceRecord` object. The `Enrich` action is called only when +`activity.IsAllDataRequested` is `true`. It contains the activity itself (which +can be enriched) and the actual `TraceRecord` from the Kusto client library. + +The following code snippet shows how to add additional tags using `Enrich`. + +```csharp +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddKustoInstrumentation(opt => opt.Enrich = (activity, record) => + { + // Add custom tags based on the TraceRecord + activity.SetTag("kusto.activity_id", record.Activity.ActivityId); + activity.SetTag("kusto.activity_type", record.Activity.ActivityType); + }) + .Build(); +``` + +[Processor](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/trace/extending-the-sdk/README.md#processor), +is the general extensibility point to add additional properties to any activity. +The `Enrich` option is specific to this instrumentation, and is provided to get +access to the `TraceRecord` object. + +#### Custom Query Summarization + +The `Enrich` callback can be used to implement custom query summarization logic. +For example, you can extract summary information from query comments: + +```csharp +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddKustoInstrumentation(opt => + { + // Disable automatic summarization + opt.RecordQuerySummary = false; + + // Extract custom summary from query comments + opt.Enrich = (activity, record) => + { + const string key = "// otel-custom-summary="; + var message = record.Message.AsSpan(); + var begin = message.IndexOf(key, StringComparison.Ordinal); + + if (begin < 0) + { + return; + } + + var summary = message.Slice(begin + key.Length); + var end = summary.IndexOfAny('\r', '\n'); + if (end < 0) + { + end = summary.Length; + } + + summary = summary.Slice(0, end).Trim(); + var summaryString = summary.ToString(); + + activity.SetTag(SemanticConventions.AttributeDbQuerySummary, summaryString); + activity.DisplayName = summaryString; + }; + }) + .Build(); +``` + +With this configuration, a query like: + +```kql +// otel-custom-summary=Get active users +Users +| where IsActive == true +| take 100 +``` + +Would result in an activity with the summary set to `"Get active users"` +and the activity display name set to the same value. + ## References * [OpenTelemetry Project](https://opentelemetry.io/) From 898ad322f48c2f787518927e079a76dfd0ef7421 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 1 Dec 2025 14:13:10 -0800 Subject: [PATCH 28/46] Separate server.address from server.port --- .../Implementation/KustoTraceListener.cs | 9 ++- .../Implementation/TraceRecordParser.cs | 62 ++++++++++++++++--- .../KustoIntegrationTests.cs | 14 ++--- ... - take 10_processQuery=False.verified.txt | 9 ++- ...e - take 10_processQuery=True.verified.txt | 9 ++- ... number=42_processQuery=False.verified.txt | 9 ++- ...t number=42_processQuery=True.verified.txt | 9 ++- .../TraceRecordParserTests.cs | 25 +++++++- .../VerifyExtensions.cs | 15 +++++ 9 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/VerifyExtensions.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index ced6351167..3660454fb1 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -76,9 +76,14 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri.ToString()); } - if (!result.Host.IsEmpty) + if (!result.ServerAddress.IsEmpty) { - activity.SetTag(SemanticConventions.AttributeServerAddress, result.Host.ToString()); + activity.SetTag(SemanticConventions.AttributeServerAddress, result.ServerAddress.ToString()); + } + + if (result.ServerPort is not null) + { + activity.SetTag(SemanticConventions.AttributeServerPort, result.ServerPort.Value); } if (!result.Database.IsEmpty) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index e94c636969..a3108a9dbc 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -5,6 +5,8 @@ using System.Buffers; #endif +using System.Globalization; + namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal class TraceRecordParser @@ -18,14 +20,14 @@ internal class TraceRecordParser public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) { var uri = ExtractValueBetween(message, "Uri="); - var host = GetServerAddress(uri); + GetServerAddressAndPort(uri, out var serverAddress, out var serverPort); var database = ExtractValueBetween(message, "DatabaseName="); // Query text may have embedded delimiters, however it is always the last field in the message // so we can just take everything after "text=" var queryText = message.SliceAfter("text="); - return new ParsedRequestStart(uri, host, database, queryText); + return new ParsedRequestStart(uri, serverAddress, serverPort, database, queryText); } public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan message) @@ -56,25 +58,67 @@ private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystac return result; } - private static ReadOnlySpan GetServerAddress(ReadOnlySpan uri) + private static void GetServerAddressAndPort(ReadOnlySpan uri, out ReadOnlySpan serverAddress, out int? serverPort) { - var result = uri.SliceAfter("://"); - result = result.SliceBefore(['/']); + var hostAndPort = uri.SliceAfter("://"); + hostAndPort = hostAndPort.SliceBefore(['/']); - return result; + // Find the port separator (last colon for IPv6 compatibility) + var colonIndex = hostAndPort.LastIndexOf(':'); + if (colonIndex > 0) + { + // Check if this is an IPv6 address (contains '[' and ']') + var openBracketIndex = hostAndPort.IndexOf('['); + var closeBracketIndex = hostAndPort.IndexOf(']'); + + if (openBracketIndex >= 0 && closeBracketIndex > openBracketIndex && colonIndex > closeBracketIndex) + { + // IPv6 address with port: [2001:db8::1]:8080 + serverAddress = hostAndPort.Slice(0, colonIndex); +#if NET + serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1), CultureInfo.InvariantCulture); +#else + serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1).ToString(), CultureInfo.InvariantCulture); +#endif + } + else if (openBracketIndex < 0) + { + // IPv4 or hostname with port: localhost:8080 + serverAddress = hostAndPort.Slice(0, colonIndex); +#if NET + serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1), CultureInfo.InvariantCulture); +#else + serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1).ToString(), CultureInfo.InvariantCulture); +#endif + } + else + { + // IPv6 address without port: [2001:db8::1] + serverAddress = hostAndPort; + serverPort = null; + } + } + else + { + // No port specified + serverAddress = hostAndPort; + serverPort = null; + } } internal readonly ref struct ParsedRequestStart { public readonly ReadOnlySpan Uri; - public readonly ReadOnlySpan Host; + public readonly ReadOnlySpan ServerAddress; + public readonly int? ServerPort; public readonly ReadOnlySpan Database; public readonly ReadOnlySpan QueryText; - public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan host, ReadOnlySpan database, ReadOnlySpan queryText) + public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan serverAddress, int? serverPort, ReadOnlySpan database, ReadOnlySpan queryText) { this.Uri = uri; - this.Host = host; + this.ServerAddress = serverAddress; + this.ServerPort = serverPort; this.Database = database; this.QueryText = queryText; } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 807d363711..efc73ea2e8 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -69,7 +69,7 @@ public async Task SuccessfulQueryTest(string query, bool processQuery) activity.Source.Name, activity.Status, activity.StatusDescription, - activity.Tags, + activity.TagObjects, activity.OperationName, activity.IdFormat, }); @@ -91,8 +91,8 @@ await Verify( Activities = activitySnapshots, Metrics = metricSnapshots, }) - .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) - .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) + .ScrubHostname(kcsb.Hostname) + .ScrubPort(this.fixture.DatabaseContainer.GetMappedPublicPort()) .UseDirectory("Snapshots") .UseParameters(query, processQuery); } @@ -137,8 +137,6 @@ public async Task FailedQueryTest(string query, bool processQuery) await Task.CompletedTask; }); - Debugger.Break(); - tracerProvider.ForceFlush(); meterProvider.ForceFlush(); @@ -150,7 +148,7 @@ public async Task FailedQueryTest(string query, bool processQuery) activity.Source.Name, activity.Status, activity.StatusDescription, - activity.Tags, + activity.TagObjects, activity.OperationName, activity.IdFormat, }); @@ -177,8 +175,8 @@ await Verify( HasMessage = !string.IsNullOrEmpty(exception.Message), }, }) - .ScrubLinesWithReplace(line => line.Replace(kcsb.Hostname, "{Hostname}")) - .ScrubLinesWithReplace(line => line.Replace(this.fixture.DatabaseContainer.GetMappedPublicPort().ToString(), "{Port}")) + .ScrubHostname(kcsb.Hostname) + .ScrubPort(this.fixture.DatabaseContainer.GetMappedPublicPort()) .UseDirectory("Snapshots") .UseParameters(query, processQuery); } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index c695a15334..d1dec5585e 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -5,7 +5,7 @@ Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', - Tags: [ + TagObjects: [ { db.system.name: azure.kusto }, @@ -16,10 +16,13 @@ db.operation.name: KD.RestClient.ExecuteQuery }, { - url.full: http://{Hostname}:{Port}/v1/rest/query + url.full: http://Scrubbed:Scrubbed/v1/rest/query }, { - server.address: {Hostname}:{Port} + server.address: Scrubbed + }, + { + server.port: Scrubbed }, { db.namespace: NetDefaultDB diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index ce38cbebc6..58e7255504 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -5,7 +5,7 @@ Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', - Tags: [ + TagObjects: [ { db.system.name: azure.kusto }, @@ -16,10 +16,13 @@ db.operation.name: KD.RestClient.ExecuteQuery }, { - url.full: http://{Hostname}:{Port}/v1/rest/query + url.full: http://Scrubbed:Scrubbed/v1/rest/query }, { - server.address: {Hostname}:{Port} + server.address: Scrubbed + }, + { + server.port: Scrubbed }, { db.namespace: NetDefaultDB diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index ceb644d456..f2df19131a 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -4,7 +4,7 @@ DisplayName: KD.RestClient.ExecuteQuery, Name: Kusto.Client, Status: Ok, - Tags: [ + TagObjects: [ { db.system.name: azure.kusto }, @@ -15,10 +15,13 @@ db.operation.name: KD.RestClient.ExecuteQuery }, { - url.full: http://{Hostname}:{Port}/v1/rest/query + url.full: http://Scrubbed:Scrubbed/v1/rest/query }, { - server.address: {Hostname}:{Port} + server.address: Scrubbed + }, + { + server.port: Scrubbed }, { db.namespace: NetDefaultDB diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt index e4c143b943..274fae0468 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt @@ -4,7 +4,7 @@ DisplayName: print, Name: Kusto.Client, Status: Ok, - Tags: [ + TagObjects: [ { db.system.name: azure.kusto }, @@ -15,10 +15,13 @@ db.operation.name: KD.RestClient.ExecuteQuery }, { - url.full: http://{Hostname}:{Port}/v1/rest/query + url.full: http://Scrubbed:Scrubbed/v1/rest/query }, { - server.address: {Hostname}:{Port} + server.address: Scrubbed + }, + { + server.port: Scrubbed }, { db.namespace: NetDefaultDB diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index d847f0b896..92c7a4a47b 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -15,7 +15,8 @@ public void ParseRequestStartSuccess() var result = TraceRecordParser.ParseRequestStart(message); Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri.ToString()); - Assert.Equal("127.0.0.1:49902", result.Host.ToString()); + Assert.Equal("127.0.0.1", result.ServerAddress.ToString()); + Assert.Equal("49902", result.ServerPort.ToString()); Assert.Equal("NetDefaultDB", result.Database.ToString()); Assert.Equal("InvalidTable | take 10 | where Col1=7 | summarize by Date, Time", result.QueryText.ToString()); } @@ -27,11 +28,31 @@ public void ParseRequestStartFailure() var result = TraceRecordParser.ParseRequestStart(message); Assert.Equal("http://", result.Uri.ToString()); - Assert.Equal(string.Empty, result.Host.ToString()); + Assert.Equal(string.Empty, result.ServerAddress.ToString()); + Assert.Equal(string.Empty, result.ServerPort.ToString()); Assert.Equal(string.Empty, result.Database.ToString()); Assert.Equal(string.Empty, result.QueryText.ToString()); } + [Theory] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://localhost/v1/rest/query, DatabaseName=TestDB, text=print 1", "localhost", null)] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://[2001:db8::1]:8080/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", 8080)] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://[2001:db8::1]/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", null)] + public void ParseRequestStartServerAddressAndPort(string message, string expectedAddress, int? expectedPort) + { + var result = TraceRecordParser.ParseRequestStart(message); + Assert.Equal(expectedAddress, result.ServerAddress.ToString()); + + if (expectedPort.HasValue) + { + Assert.Equal(expectedPort.Value, result.ServerPort); + } + else + { + Assert.Null(result.ServerPort); + } + } + [Fact] public void ParseActivityComplete() { diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/VerifyExtensions.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/VerifyExtensions.cs new file mode 100644 index 0000000000..3f7b36a350 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/VerifyExtensions.cs @@ -0,0 +1,15 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +internal static class VerifyExtensions +{ + public static SettingsTask ScrubHostname(this SettingsTask settings, string hostname) => + settings.ScrubLinesWithReplace(line => line.Replace(hostname, "Scrubbed")); + + public static SettingsTask ScrubPort(this SettingsTask settings, int port) => + settings + .ScrubLinesWithReplace(line => line.Replace(port.ToString(), "Scrubbed")) + .ScrubInstance(i => i == port); +} From 756421a1dc4965a64a880198f2b0c3adef969657 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 1 Dec 2025 14:48:35 -0800 Subject: [PATCH 29/46] Replace custom Uri parsing with Uri.TryCreate --- .../Implementation/KustoTraceListener.cs | 8 +-- .../Implementation/TraceRecordParser.cs | 62 ++----------------- .../TraceRecordParserTests.cs | 24 +++---- 3 files changed, 18 insertions(+), 76 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 3660454fb1..212d8fc962 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -71,14 +71,14 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); - if (!result.Uri.IsEmpty) + if (!string.IsNullOrEmpty(result.Uri)) { - activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri.ToString()); + activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri); } - if (!result.ServerAddress.IsEmpty) + if (!string.IsNullOrEmpty(result.ServerAddress)) { - activity.SetTag(SemanticConventions.AttributeServerAddress, result.ServerAddress.ToString()); + activity.SetTag(SemanticConventions.AttributeServerAddress, result.ServerAddress); } if (result.ServerPort is not null) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index a3108a9dbc..d6eb98cb13 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -5,8 +5,6 @@ using System.Buffers; #endif -using System.Globalization; - namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal class TraceRecordParser @@ -19,15 +17,15 @@ internal class TraceRecordParser public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) { - var uri = ExtractValueBetween(message, "Uri="); - GetServerAddressAndPort(uri, out var serverAddress, out var serverPort); + var uri = ExtractValueBetween(message, "Uri=").ToString(); + Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsed); var database = ExtractValueBetween(message, "DatabaseName="); // Query text may have embedded delimiters, however it is always the last field in the message // so we can just take everything after "text=" var queryText = message.SliceAfter("text="); - return new ParsedRequestStart(uri, serverAddress, serverPort, database, queryText); + return new ParsedRequestStart(uri, parsed?.Host, parsed?.Port, database, queryText); } public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan message) @@ -58,63 +56,15 @@ private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystac return result; } - private static void GetServerAddressAndPort(ReadOnlySpan uri, out ReadOnlySpan serverAddress, out int? serverPort) - { - var hostAndPort = uri.SliceAfter("://"); - hostAndPort = hostAndPort.SliceBefore(['/']); - - // Find the port separator (last colon for IPv6 compatibility) - var colonIndex = hostAndPort.LastIndexOf(':'); - if (colonIndex > 0) - { - // Check if this is an IPv6 address (contains '[' and ']') - var openBracketIndex = hostAndPort.IndexOf('['); - var closeBracketIndex = hostAndPort.IndexOf(']'); - - if (openBracketIndex >= 0 && closeBracketIndex > openBracketIndex && colonIndex > closeBracketIndex) - { - // IPv6 address with port: [2001:db8::1]:8080 - serverAddress = hostAndPort.Slice(0, colonIndex); -#if NET - serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1), CultureInfo.InvariantCulture); -#else - serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1).ToString(), CultureInfo.InvariantCulture); -#endif - } - else if (openBracketIndex < 0) - { - // IPv4 or hostname with port: localhost:8080 - serverAddress = hostAndPort.Slice(0, colonIndex); -#if NET - serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1), CultureInfo.InvariantCulture); -#else - serverPort = int.Parse(hostAndPort.Slice(colonIndex + 1).ToString(), CultureInfo.InvariantCulture); -#endif - } - else - { - // IPv6 address without port: [2001:db8::1] - serverAddress = hostAndPort; - serverPort = null; - } - } - else - { - // No port specified - serverAddress = hostAndPort; - serverPort = null; - } - } - internal readonly ref struct ParsedRequestStart { - public readonly ReadOnlySpan Uri; - public readonly ReadOnlySpan ServerAddress; + public readonly string Uri; + public readonly string? ServerAddress; public readonly int? ServerPort; public readonly ReadOnlySpan Database; public readonly ReadOnlySpan QueryText; - public ParsedRequestStart(ReadOnlySpan uri, ReadOnlySpan serverAddress, int? serverPort, ReadOnlySpan database, ReadOnlySpan queryText) + public ParsedRequestStart(string uri, string? serverAddress, int? serverPort, ReadOnlySpan database, ReadOnlySpan queryText) { this.Uri = uri; this.ServerAddress = serverAddress; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 92c7a4a47b..7281686b73 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -14,8 +14,8 @@ public void ParseRequestStartSuccess() const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; var result = TraceRecordParser.ParseRequestStart(message); - Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri.ToString()); - Assert.Equal("127.0.0.1", result.ServerAddress.ToString()); + Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri); + Assert.Equal("127.0.0.1", result.ServerAddress); Assert.Equal("49902", result.ServerPort.ToString()); Assert.Equal("NetDefaultDB", result.Database.ToString()); Assert.Equal("InvalidTable | take 10 | where Col1=7 | summarize by Date, Time", result.QueryText.ToString()); @@ -27,30 +27,22 @@ public void ParseRequestStartFailure() const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://"; var result = TraceRecordParser.ParseRequestStart(message); - Assert.Equal("http://", result.Uri.ToString()); - Assert.Equal(string.Empty, result.ServerAddress.ToString()); + Assert.Equal("http://", result.Uri); + Assert.Null(result.ServerAddress); Assert.Equal(string.Empty, result.ServerPort.ToString()); Assert.Equal(string.Empty, result.Database.ToString()); Assert.Equal(string.Empty, result.QueryText.ToString()); } [Theory] - [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://localhost/v1/rest/query, DatabaseName=TestDB, text=print 1", "localhost", null)] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://localhost/v1/rest/query, DatabaseName=TestDB, text=print 1", "localhost", 80)] [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://[2001:db8::1]:8080/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", 8080)] - [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://[2001:db8::1]/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", null)] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=https://[2001:db8::1]/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", 443)] public void ParseRequestStartServerAddressAndPort(string message, string expectedAddress, int? expectedPort) { var result = TraceRecordParser.ParseRequestStart(message); - Assert.Equal(expectedAddress, result.ServerAddress.ToString()); - - if (expectedPort.HasValue) - { - Assert.Equal(expectedPort.Value, result.ServerPort); - } - else - { - Assert.Null(result.ServerPort); - } + Assert.Equal(expectedAddress, result.ServerAddress); + Assert.Equal(expectedPort, result.ServerPort); } [Fact] From 7873ff6d3a03a207c3147530b1ac9c0301a2558d Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 1 Dec 2025 16:31:26 -0800 Subject: [PATCH 30/46] Add full instrumentation benchmark --- .../InstrumentationBenchmarks.cs | 163 ++++++++++++++++++ .../README.md | 24 ++- 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs new file mode 100644 index 0000000000..1c26ec2c24 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs @@ -0,0 +1,163 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Benchmarks; + +/// +/// Benchmarks that simulate end-to-end trace and metrics instrumentation by manually creating TraceRecords +/// and passing them through both the trace listener and metrics listener. +/// +[MemoryDiagnoser] +public class InstrumentationBenchmarks +{ + private static readonly KustoUtils.ActivityType TestActivityType = new FakeActivtyType(); + + private readonly Guid activityId = Guid.NewGuid(); + private readonly string clientRequestId = "SW52YWxpZFRhYmxlIHwgdGFrZSAxMA=="; + + private KustoTraceListener? traceListener; + private KustoMetricListener? metricListener; + private KustoUtils.TraceRecord requestStartRecord = null!; + private KustoUtils.TraceRecord activityCompleteRecord = null!; + private KustoUtils.TraceRecord exceptionRecord = null!; + private TracerProvider? tracerProvider; + private MeterProvider? meterProvider; + private IDisposable? tracingHandle; + private IDisposable? metricHandle; + + [GlobalSetup] + public void Setup() + { + // Setup TracerProvider with the Kusto activity source + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource(KustoActivitySourceHelper.ActivitySourceName) + .Build(); + + // Setup MeterProvider with the Kusto meter + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(KustoActivitySourceHelper.MeterName) + .Build(); + + // Activate instrumentation handles + this.tracingHandle = KustoInstrumentation.HandleManager.AddTracingHandle(); + this.metricHandle = KustoInstrumentation.HandleManager.AddMetricHandle(); + + // Create listeners + this.traceListener = new KustoTraceListener(); + this.metricListener = new KustoMetricListener(); + + // Create TraceRecord instances that simulate a query execution flow + this.requestStartRecord = CreateRequestStartRecord( + this.activityId, + this.clientRequestId, + "StormEvents | take 10 | where Col1 = 7 | summarize by Date, Time"); + + this.activityCompleteRecord = CreateActivityCompleteRecord( + this.activityId, + this.clientRequestId); + + this.exceptionRecord = CreateExceptionRecord( + this.activityId, + this.clientRequestId); + } + + [GlobalCleanup] + public void Cleanup() + { + this.tracingHandle?.Dispose(); + this.metricHandle?.Dispose(); + this.tracerProvider?.Dispose(); + this.meterProvider?.Dispose(); + } + + [Benchmark] + public void SuccessfulQuery() + { + // Simulate a successful query execution + this.traceListener!.Write(this.requestStartRecord); + this.metricListener!.Write(this.requestStartRecord); + + this.traceListener.Write(this.activityCompleteRecord); + this.metricListener.Write(this.activityCompleteRecord); + } + + [Benchmark] + public void FailedQuery() + { + // Simulate a failed query execution + this.traceListener!.Write(this.requestStartRecord); + this.metricListener!.Write(this.requestStartRecord); + + this.traceListener.Write(this.exceptionRecord); + + this.traceListener.Write(this.activityCompleteRecord); + this.metricListener.Write(this.activityCompleteRecord); + } + + [Benchmark] + public void TraceListenerOnly() + { + // Benchmark just the trace listener + this.traceListener!.Write(this.requestStartRecord); + this.traceListener.Write(this.activityCompleteRecord); + } + + [Benchmark] + public void MetricListenerOnly() + { + // Benchmark just the metric listener + this.metricListener!.Write(this.requestStartRecord); + this.metricListener.Write(this.activityCompleteRecord); + } + + private static KustoUtils.TraceRecord CreateRequestStartRecord(Guid activityId, string clientRequestId, string queryText) + { + var message = $$"""$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/query, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\benchmarkuser, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId={{clientRequestId}}, text={{queryText}}"""; + using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + return KustoUtils.TraceRecord.Create("Kusto.Data", KustoUtils.TraceVerbosity.Verbose, message); + } + + private static KustoUtils.TraceRecord CreateActivityCompleteRecord(Guid activityId, string clientRequestId) + { + const string message = "MonitoredActivityCompletedSuccessfully: TestActivityType=KD.RestClient.ExecuteQuery, Timestamp=2025-12-01T02:30:30.0211167Z, ParentActivityId={0}, Duration=4316.802 [ms], HowEnded=Success"; + using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + return KustoUtils.TraceRecord.Create("Kusto.Data", KustoUtils.TraceVerbosity.Verbose, message); + } + + private static KustoUtils.TraceRecord CreateExceptionRecord(Guid activityId, string clientRequestId) + { + var message = + $""" + Exception object created: Kusto.Data.Exceptions.SemanticException + [0]Kusto.Data.Exceptions.SemanticException: Semantic error: 'take' operator: Failed to resolve table or column expression named 'InvalidTable' + Timestamp=2025-12-01T02:39:36.3878585Z + ClientRequestId={clientRequestId} + ActivityId={activityId} + ActivityType=KD.RestClient.ExecuteQuery + ErrorCode=SEM0100 + ErrorReason=BadRequest + ErrorMessage='take' operator: Failed to resolve table or column expression named 'InvalidTable' + DataSource=http://127.0.0.1:62413/v1/rest/query + DatabaseName=NetDefaultDB + """; + using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + return KustoUtils.TraceRecord.Create("KD.Exceptions", KustoUtils.TraceVerbosity.Error, message); + } + + private class FakeActivtyType : KustoUtils.ActivityType + { + public FakeActivtyType() + : base("FakeActivity", "A fake activity", KustoUtils.TraceVerbosity.Info, KustoUtils.TraceVerbosity.Info, KustoUtils.TraceVerbosity.Info) + { + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 581cea77dd..8b30ffeb5f 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -18,7 +18,29 @@ option number from the list of options shown on the Console window. ## Results -// TODO: Includes private fixes in Kusto-Query-Langugage +### Full instrumentation + +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.7092/24H2/2024Update/HudsonValley) +Intel Core i9-10940X CPU 3.30GHz (Max: 3.31GHz), 1 CPU, 28 logical and 14 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + + +``` +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------- |-------------:|-----------:|-----------:|-------:|-------:|----------:| +| SuccessfulQuery | 12,866.04 ns | 252.865 ns | 378.477 ns | 1.1597 | 0.0153 | 11792 B | +| FailedQuery | 13,736.88 ns | 271.636 ns | 453.842 ns | 1.1902 | 0.0153 | 11984 B | +| TraceListenerOnly | 13,281.18 ns | 261.834 ns | 311.695 ns | 1.1444 | 0.0153 | 11592 B | +| MetricListenerOnly | 88.40 ns | 1.783 ns | 2.318 ns | 0.0095 | - | 96 B | + +### Summarization and sanitization processing + +Summarization and sanitization are the most expensive parts of instrumentation, so there are benchmarks to measure their +specific cost. ``` From d17d4034173beaab79f48d71ee60086cefc6ccc6 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Tue, 2 Dec 2025 11:00:36 -0800 Subject: [PATCH 31/46] Update README with metrics --- src/OpenTelemetry.Instrumentation.Kusto/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index c267b15253..b23f8a3aa4 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -75,6 +75,17 @@ public class Program } ``` +##### List of metrics produced + +The instrumentation is implemented based on [metrics semantic +conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/database/database-metrics.md#database-operation). +Currently, the instrumentation supports the following metrics. + +| Name | Instrument Type | Unit | Description | +|--------------------------------|-----------------|---------------|-----------------------------------------| +| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations. | +| `db.client.operation.count` | Counter | `{operation}` | Number of database client operations. | + ## Advanced configuration This instrumentation can be configured to change the default behavior by using From 552e7fcae0aaeaa31dbd6a29041989ddd1a6e7e5 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Tue, 2 Dec 2025 16:29:00 -0800 Subject: [PATCH 32/46] Add error.type as attribute on trace --- .../Implementation/KustoTraceListener.cs | 15 ++++++++++++++- .../Implementation/TraceRecordParser.cs | 7 +++++-- .../KustoIntegrationTests.cs | 2 +- ...able - take 10_processQuery=False.verified.txt | 5 ++++- ...Table - take 10_processQuery=True.verified.txt | 5 ++++- .../TraceRecordParserTests.cs | 4 +++- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs index 212d8fc962..bbeed9cce6 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs @@ -49,8 +49,21 @@ public override void Write(KustoUtils.TraceRecord record) private void HandleException(KustoUtils.TraceRecord record) { var activity = this.GetActivity(record); + + if (activity is null) + { + return; + } + var result = TraceRecordParser.ParseException(record.Message.AsSpan()); - activity?.SetStatus(ActivityStatusCode.Error, result.ErrorMessage.ToString()); + + if (!result.ErrorType.IsEmpty) + { + activity.AddTag(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); + } + + var description = result.ErrorMessage.IsEmpty ? null : result.ErrorMessage.ToString(); + activity.SetStatus(ActivityStatusCode.Error, description); } private void HandleHttpRequestStart(KustoUtils.TraceRecord record) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index d6eb98cb13..304569b905 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -37,7 +37,8 @@ public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan me public static ParsedException ParseException(ReadOnlySpan message) { var errorMessage = ExtractValueBetween(message, "ErrorMessage="); - return new ParsedException(errorMessage); + var errorType = ExtractValueBetween(message, "Exception object created: "); + return new ParsedException(errorMessage, errorType); } private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystack, ReadOnlySpan needle) @@ -87,10 +88,12 @@ public ParsedActivityComplete(ReadOnlySpan howEnded) internal readonly ref struct ParsedException { public readonly ReadOnlySpan ErrorMessage; + public readonly ReadOnlySpan ErrorType; - public ParsedException(ReadOnlySpan errorMessage) + public ParsedException(ReadOnlySpan errorMessage, ReadOnlySpan errorType) { this.ErrorMessage = errorMessage; + this.ErrorType = errorType; } } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index efc73ea2e8..4f89569034 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -171,7 +171,7 @@ await Verify( Metrics = metricSnapshots, Exception = new { - Type = exception.GetType().Name, + Type = exception.GetType().FullName, HasMessage = !string.IsNullOrEmpty(exception.Message), }, }) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index d1dec5585e..ac35b3a491 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -26,6 +26,9 @@ }, { db.namespace: NetDefaultDB + }, + { + error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, @@ -47,7 +50,7 @@ } ], Exception: { - Type: SemanticException, + Type: Kusto.Data.Exceptions.SemanticException, HasMessage: true } } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 58e7255504..7b3f5aeb0b 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -32,6 +32,9 @@ }, { db.query.summary: InvalidTable | take + }, + { + error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, @@ -53,7 +56,7 @@ } ], Exception: { - Type: SemanticException, + Type: Kusto.Data.Exceptions.SemanticException, HasMessage: true } } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 7281686b73..4cb53652b6 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -159,14 +159,16 @@ at System.Threading.Thread.StartCallback() var result = TraceRecordParser.ParseException(message); Assert.Equal("'take' operator: Failed to resolve table or column expression named 'InvalidTable'", result.ErrorMessage.ToString()); + Assert.Equal("Kusto.Data.Exceptions.SemanticException", result.ErrorType.ToString()); } [Fact] public void ParseExceptionFailure() { - const string message = "Exception object created: Kusto.Data.Exceptions.SemanticException Timestamp=2025-12-01T02:39:36.3878585Z"; + const string message = "ProcessName=testhost Timestamp=2025-12-01T02:39:36.3878585Z"; var result = TraceRecordParser.ParseException(message); Assert.Equal(string.Empty, result.ErrorMessage.ToString()); + Assert.Equal(string.Empty, result.ErrorType.ToString()); } } From c3d292341892c47949a1a98277a3ed1bfd5befda Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 10:09:51 -0800 Subject: [PATCH 33/46] Fix semantic convention for metric name --- .../Implementation/KustoActivitySourceHelper.cs | 3 +-- src/Shared/SemanticConventions.cs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs index c6c2303929..30e4e9fcc0 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs @@ -5,7 +5,6 @@ using System.Diagnostics.Metrics; using System.Reflection; using OpenTelemetry.Internal; -using OpenTelemetry.Trace; namespace OpenTelemetry.Instrumentation.Kusto.Implementation; @@ -25,7 +24,7 @@ internal static class KustoActivitySourceHelper public static readonly Meter Meter = new(MeterName, PackageVersion); public static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( - SemanticConventions.AttributeDbClientOperationDuration, + "db.client.operation.duration", unit: "s", advice: new InstrumentAdvice() { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }, description: "Duration of database client operations"); diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index dd4e911649..235b790457 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -141,7 +141,6 @@ internal static class SemanticConventions // v1.36.0 database conventions: // https://github.com/open-telemetry/semantic-conventions/tree/v1.36.0/docs/database - public const string AttributeDbClientOperationDuration = "db.client.operation.duration"; public const string AttributeDbCollectionName = "db.collection.name"; public const string AttributeDbOperationName = "db.operation.name"; public const string AttributeDbSystemName = "db.system.name"; From c709cf369e41b44bf2a8bfa1aeb195a8668fd655 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 12:09:01 -0800 Subject: [PATCH 34/46] Refactor so that metrics attributes match spans --- .../Implementation/ActivityExtensions.cs | 22 ++ .../KustoActivitySourceHelper.cs | 4 - .../Implementation/KustoInstrumentation.cs | 24 +- .../Implementation/KustoMetricListener.cs | 75 ------ .../Implementation/KustoTraceListener.cs | 188 --------------- .../KustoTraceRecordListener.cs | 219 ++++++++++++++++++ .../README.md | 25 +- .../InstrumentationBenchmarks.cs | 41 ++-- ... - take 10_processQuery=False.verified.txt | 12 +- ...e - take 10_processQuery=True.verified.txt | 12 +- ... number=42_processQuery=False.verified.txt | 6 - ...t number=42_processQuery=True.verified.txt | 6 - 12 files changed, 281 insertions(+), 353 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs new file mode 100644 index 0000000000..3135c32339 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal static class ActivityExtensions +{ + public static Activity AddTags(this Activity activity, TagList tags) + { + foreach (var tag in tags) + { + if (activity.GetTagItem(tag.Key) is null) + { + activity.AddTag(tag.Key, tag.Value); + } + } + + return activity; + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs index 30e4e9fcc0..0ffc2af507 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs @@ -28,8 +28,4 @@ internal static class KustoActivitySourceHelper unit: "s", advice: new InstrumentAdvice() { HistogramBucketBoundaries = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10] }, description: "Duration of database client operations"); - - public static readonly Counter OperationCounter = Meter.CreateCounter( - "db.client.operation.count", - description: "Number of database client operations"); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs index 7f02d80fd9..da0de699e6 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs @@ -7,21 +7,11 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal static class KustoInstrumentation { - private static readonly Lazy TraceListener = new(() => + private static readonly Lazy Listener = new(() => { Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); - var listener = new KustoTraceListener(); - TraceSourceManager.AddTraceListener(listener, startupDone: true); - - return listener; - }); - - private static readonly Lazy MetricListener = new(() => - { - Environment.SetEnvironmentVariable("KUSTO_DATA_TRACE_REQUEST_BODY", "1"); - - var listener = new KustoMetricListener(); + var listener = new KustoTraceRecordListener(); TraceSourceManager.AddTraceListener(listener, startupDone: true); return listener; @@ -33,13 +23,7 @@ internal static class KustoInstrumentation public static InstrumentationHandleManager HandleManager { get; } = new InstrumentationHandleManager(); - public static void InitializeTracing() - { - _ = TraceListener.Value; - } + public static void InitializeTracing() => _ = Listener.Value; - public static void InitializeMetrics() - { - _ = MetricListener.Value; - } + public static void InitializeMetrics() => _ = Listener.Value; } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs deleted file mode 100644 index 464c4f6b08..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoMetricListener.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; -using OpenTelemetry.Trace; -using KustoUtils = Kusto.Cloud.Platform.Utils; - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -internal sealed class KustoMetricListener : KustoUtils.ITraceListener -{ - private readonly AsyncLocal beginTimestamp = new(); - - public override string Name => nameof(KustoMetricListener); - - public override bool IsThreadSafe => true; - - public override void Flush() - { - } - - public override void Write(KustoUtils.TraceRecord record) - { - if (record?.Message is null) - { - return; - } - - if (!KustoInstrumentation.HandleManager.IsMetricsActive()) - { - return; - } - - if (record.IsRequestStart()) - { - this.HandleHttpRequestStart(record); - } - else if (record.IsActivityComplete()) - { - this.HangleActivityComplete(record); - } - } - - private static double GetElaspedTime(long begin) - { -#if NET - var duration = Stopwatch.GetElapsedTime(begin); -#else - var end = Stopwatch.GetTimestamp(); - var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - var delta = end - begin; - var ticks = (long)(timestampToTicks * delta); - var duration = new TimeSpan(ticks); -#endif - - return duration.TotalSeconds; - } - - private void HangleActivityComplete(KustoUtils.TraceRecord record) - { - var operationName = record.Activity.ActivityType; - var duration = GetElaspedTime(this.beginTimestamp.Value); - - var tags = new TagList - { - { SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem }, - { SemanticConventions.AttributeDbOperationName, operationName }, - }; - - KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, tags); - KustoActivitySourceHelper.OperationCounter.Add(1, tags); - } - - private void HandleHttpRequestStart(KustoUtils.TraceRecord record) => this.beginTimestamp.Value = Stopwatch.GetTimestamp(); -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs deleted file mode 100644 index bbeed9cce6..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceListener.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Collections.Concurrent; -using System.Diagnostics; -using OpenTelemetry.Trace; -using KustoUtils = Kusto.Cloud.Platform.Utils; - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -internal sealed class KustoTraceListener : KustoUtils.ITraceListener -{ - private readonly ConcurrentDictionary activities = new(); - - public override string Name => nameof(KustoTraceListener); - - public override bool IsThreadSafe => true; - - public override void Flush() - { - } - - public override void Write(KustoUtils.TraceRecord record) - { - if (record?.Message is null) - { - return; - } - - if (!KustoInstrumentation.HandleManager.IsTracingActive()) - { - return; - } - - if (record.IsRequestStart()) - { - this.HandleHttpRequestStart(record); - } - else if (record.IsActivityComplete()) - { - this.HandleActivityComplete(record); - } - else if (record.IsException()) - { - this.HandleException(record); - } - } - - private void HandleException(KustoUtils.TraceRecord record) - { - var activity = this.GetActivity(record); - - if (activity is null) - { - return; - } - - var result = TraceRecordParser.ParseException(record.Message.AsSpan()); - - if (!result.ErrorType.IsEmpty) - { - activity.AddTag(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); - } - - var description = result.ErrorMessage.IsEmpty ? null : result.ErrorMessage.ToString(); - activity.SetStatus(ActivityStatusCode.Error, description); - } - - private void HandleHttpRequestStart(KustoUtils.TraceRecord record) - { - var operationName = record.Activity.ActivityType; - - var activity = KustoActivitySourceHelper.ActivitySource.StartActivity(operationName, ActivityKind.Client); - if (activity is not null) - { - this.activities[record.Activity.ActivityId] = activity; - } - - if (activity?.IsAllDataRequested is true) - { - activity.SetTag(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); - activity.SetTag(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); - activity.SetTag(SemanticConventions.AttributeDbOperationName, operationName); - - var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); - - if (!string.IsNullOrEmpty(result.Uri)) - { - activity.SetTag(SemanticConventions.AttributeUrlFull, result.Uri); - } - - if (!string.IsNullOrEmpty(result.ServerAddress)) - { - activity.SetTag(SemanticConventions.AttributeServerAddress, result.ServerAddress); - } - - if (result.ServerPort is not null) - { - activity.SetTag(SemanticConventions.AttributeServerPort, result.ServerPort.Value); - } - - if (!result.Database.IsEmpty) - { - activity.SetTag(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); - } - - if (!result.QueryText.IsEmpty) - { - var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, result.QueryText.ToString()); - - if (KustoInstrumentation.TracingOptions.RecordQueryText) - { - activity.SetTag(SemanticConventions.AttributeDbQueryText, info.Sanitized); - } - - // Set query summary and use it as display name per spec - if (!string.IsNullOrEmpty(info.Summarized)) - { - activity.SetTag(SemanticConventions.AttributeDbQuerySummary, info.Summarized); - activity.DisplayName = info.Summarized!; - } - else - { - // Fall back to operation name if no summary available - activity.DisplayName = operationName; - } - } - else - { - // Fall back to operation name if no query text - activity.DisplayName = operationName; - } - - try - { - KustoInstrumentation.TracingOptions.Enrich?.Invoke(activity, record); - } - catch (Exception ex) - { - KustoInstrumentationEventSource.Log.EnrichmentException(ex); - } - } - } - - private void HandleActivityComplete(KustoUtils.TraceRecord record) - { - var activity = this.GetActivity(record); - if (activity is null) - { - return; - } - - var clientRequestId = record.Activity.ClientRequestId; - var activityClientRequestId = activity.GetTagItem(KustoActivitySourceHelper.ClientRequestIdTagKey) as string; - - if (clientRequestId.Equals(activityClientRequestId, StringComparison.Ordinal)) - { - var result = TraceRecordParser.ParseActivityComplete(record.Message.AsSpan()); - if (result.HowEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) - { - activity.SetStatus(ActivityStatusCode.Ok); - } - - activity.Stop(); - } - -#if NET - this.activities.Remove(record.Activity.ActivityId, out _); -#else - ((IDictionary)this.activities).Remove(record.Activity.ActivityId); -#endif - } - - private Activity? GetActivity(KustoUtils.TraceRecord record) - { - if (Activity.Current is not null) - { - return Activity.Current; - } - - if (this.activities.TryGetValue(record.Activity.ActivityId, out var activity)) - { - return activity; - } - - return null; - } -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs new file mode 100644 index 0000000000..a1775688f3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -0,0 +1,219 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Collections.Concurrent; +using System.Diagnostics; +using OpenTelemetry.Trace; +using KustoUtils = Kusto.Cloud.Platform.Utils; + +namespace OpenTelemetry.Instrumentation.Kusto.Implementation; + +internal sealed class KustoTraceRecordListener : KustoUtils.ITraceListener +{ + // The client's async machinery may not call us back using the same AsyncLocal context, so we must manually track + // the Activity's ActivityId (which the client guarantees will be unique) with the context data we need. + private readonly ConcurrentDictionary contexts = new(); + + public override string Name { get; } = nameof(KustoTraceRecordListener); + + public override bool IsThreadSafe => true; + + public override void Flush() + { + } + + public override void Write(KustoUtils.TraceRecord record) + { + if (record?.Message is null) + { + return; + } + + if (!KustoInstrumentation.HandleManager.IsTracingActive() && !KustoInstrumentation.HandleManager.IsMetricsActive()) + { + return; + } + + if (record.IsRequestStart()) + { + this.HandleHttpRequestStart(record); + } + else if (record.IsActivityComplete()) + { + this.HandleActivityComplete(record); + } + else if (record.IsException()) + { + this.HandleException(record); + } + } + + private static double GetElapsedTime(long begin) + { +#if NET + var duration = Stopwatch.GetElapsedTime(begin); +#else + var end = Stopwatch.GetTimestamp(); + var timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + var delta = end - begin; + var ticks = (long)(timestampToTicks * delta); + var duration = new TimeSpan(ticks); +#endif + + return duration.TotalSeconds; + } + + private static bool ShouldComputeTags(Activity? activity) => + (activity is not null && activity.IsAllDataRequested) || KustoInstrumentation.HandleManager.IsMetricsActive(); + + private void CallEnrichment(KustoUtils.TraceRecord record) + { + try + { + var activity = this.GetContext(record)?.Activity; + if (activity is not null && activity.IsAllDataRequested) + { + KustoInstrumentation.TracingOptions.Enrich?.Invoke(activity, record); + } + } + catch (Exception ex) + { + KustoInstrumentationEventSource.Log.EnrichmentException(ex); + } + } + + private void HandleException(KustoUtils.TraceRecord record) + { + var context = this.GetContext(record); + var activity = context?.Activity; + if (context is null) + { + return; + } + + var result = TraceRecordParser.ParseException(record.Message.AsSpan()); + if (!result.ErrorType.IsEmpty) + { + activity?.SetTag(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); + context.Value.Tags.Add(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); + } + + var description = result.ErrorMessage.IsEmpty ? null : result.ErrorMessage.ToString(); + activity?.SetStatus(ActivityStatusCode.Error, description); + + this.CallEnrichment(record); + } + + private void HandleHttpRequestStart(KustoUtils.TraceRecord record) + { + var beginTimestamp = Stopwatch.GetTimestamp(); + var operationName = record.Activity.ActivityType; + + var activity = KustoActivitySourceHelper.ActivitySource.StartActivity(operationName, ActivityKind.Client); + var tagList = default(TagList); + + if (ShouldComputeTags(activity)) + { + activity?.DisplayName = operationName; + + tagList.Add(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); + tagList.Add(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); + tagList.Add(SemanticConventions.AttributeDbOperationName, operationName); + + var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); + + if (!string.IsNullOrEmpty(result.Uri)) + { + tagList.Add(SemanticConventions.AttributeUrlFull, result.Uri); + } + + if (!string.IsNullOrEmpty(result.ServerAddress)) + { + tagList.Add(SemanticConventions.AttributeServerAddress, result.ServerAddress); + } + + if (result.ServerPort is not null) + { + tagList.Add(SemanticConventions.AttributeServerPort, result.ServerPort.Value); + } + + if (!result.Database.IsEmpty) + { + tagList.Add(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); + } + + if (!result.QueryText.IsEmpty) + { + var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, result.QueryText.ToString()); + + if (KustoInstrumentation.TracingOptions.RecordQueryText) + { + tagList.Add(SemanticConventions.AttributeDbQueryText, info.Sanitized); + } + + // Set query summary and use it as display name per spec + if (!string.IsNullOrEmpty(info.Summarized)) + { + tagList.Add(SemanticConventions.AttributeDbQuerySummary, info.Summarized); + activity?.DisplayName = info.Summarized!; + } + } + } + + this.contexts[record.Activity.ActivityId] = new ContextData(beginTimestamp, tagList, activity!); + + this.CallEnrichment(record); + } + + private void HandleActivityComplete(KustoUtils.TraceRecord record) + { + var context = this.GetContext(record); + if (context is null) + { + return; + } + + var activity = context.Value.Activity; + + var result = TraceRecordParser.ParseActivityComplete(record.Message.AsSpan()); + if (result.HowEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) + { + activity.SetStatus(ActivityStatusCode.Ok); + } + + activity.AddTags(context.Value.Tags); + this.CallEnrichment(record); + activity.Stop(); + + var duration = activity?.Duration.TotalSeconds ?? GetElapsedTime(context.Value.BeginTimestamp); + KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, context.Value.Tags); + + this.contexts.TryRemove(record.Activity.ActivityId, out _); + } + + private ContextData? GetContext(KustoUtils.TraceRecord record) + { + if (this.contexts.TryGetValue(record.Activity.ActivityId, out var context)) + { + return context; + } + + return null; + } + + private readonly struct ContextData + { + public ContextData(long beginTimestamp, TagList tags, Activity activity) + { + this.BeginTimestamp = beginTimestamp; + this.Tags = tags; + this.Activity = activity; + } + + public long BeginTimestamp { get; } + + public TagList Tags { get; } + + public Activity Activity { get; } + } +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index b23f8a3aa4..7b47a14bd1 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -78,13 +78,15 @@ public class Program ##### List of metrics produced The instrumentation is implemented based on [metrics semantic -conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.29.0/docs/database/database-metrics.md#database-operation). -Currently, the instrumentation supports the following metrics. +conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/database/database-metrics.md). +Currently, the instrumentation supports the following metric: -| Name | Instrument Type | Unit | Description | -|--------------------------------|-----------------|---------------|-----------------------------------------| -| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations. | -| `db.client.operation.count` | Counter | `{operation}` | Number of database client operations. | +| Name | Instrument Type | Unit | Description | Attributes | +|--------------------------------|-----------------|------|-----------------------------------------|------------| +| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations. | `db.system`, `db.operation.name`, `db.namespace`, `db.query.summary`¹, `server.address`, `server.port`, `error.type`² | + +¹ `db.query.summary` is only included when `RecordQuerySummary` is enabled (default: `true`) +² `error.type` is only included when an error occurs ## Advanced configuration @@ -135,7 +137,6 @@ The following code snippet shows how to add additional tags using `Enrich`. ```csharp using OpenTelemetry.Instrumentation.Kusto.Implementation; -using KustoUtils = Kusto.Cloud.Platform.Utils; using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddKustoInstrumentation(opt => opt.Enrich = (activity, record) => @@ -159,7 +160,6 @@ For example, you can extract summary information from query comments: ```csharp using OpenTelemetry.Instrumentation.Kusto.Implementation; -using KustoUtils = Kusto.Cloud.Platform.Utils; using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddKustoInstrumentation(opt => @@ -189,7 +189,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() summary = summary.Slice(0, end).Trim(); var summaryString = summary.ToString(); - activity.SetTag(SemanticConventions.AttributeDbQuerySummary, summaryString); + activity.SetTag("db.query.summary", summaryString); activity.DisplayName = summaryString; }; }) @@ -205,12 +205,11 @@ Users | take 100 ``` -Would result in an activity with the summary set to `"Get active users"` -and the activity display name set to the same value. +Would result in an activity with the summary and display name set to `"Get active users"`. ## References * [OpenTelemetry Project](https://opentelemetry.io/) * [Azure Data Explorer (Kusto)](https://docs.microsoft.com/azure/data-explorer/) -* [OpenTelemetry semantic conventions for database - calls](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md) +* [OpenTelemetry semantic conventions for database spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/database/database-spans.md) +* [OpenTelemetry semantic conventions for database metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/database/database-metrics.md) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs index 1c26ec2c24..fa09d4ad1b 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs @@ -21,8 +21,7 @@ public class InstrumentationBenchmarks private readonly Guid activityId = Guid.NewGuid(); private readonly string clientRequestId = "SW52YWxpZFRhYmxlIHwgdGFrZSAxMA=="; - private KustoTraceListener? traceListener; - private KustoMetricListener? metricListener; + private KustoTraceRecordListener? listener; private KustoUtils.TraceRecord requestStartRecord = null!; private KustoUtils.TraceRecord activityCompleteRecord = null!; private KustoUtils.TraceRecord exceptionRecord = null!; @@ -48,9 +47,8 @@ public void Setup() this.tracingHandle = KustoInstrumentation.HandleManager.AddTracingHandle(); this.metricHandle = KustoInstrumentation.HandleManager.AddMetricHandle(); - // Create listeners - this.traceListener = new KustoTraceListener(); - this.metricListener = new KustoMetricListener(); + // Create single listener for both traces and metrics + this.listener = new KustoTraceRecordListener(); // Create TraceRecord instances that simulate a query execution flow this.requestStartRecord = CreateRequestStartRecord( @@ -80,40 +78,37 @@ public void Cleanup() public void SuccessfulQuery() { // Simulate a successful query execution - this.traceListener!.Write(this.requestStartRecord); - this.metricListener!.Write(this.requestStartRecord); - - this.traceListener.Write(this.activityCompleteRecord); - this.metricListener.Write(this.activityCompleteRecord); + this.listener!.Write(this.requestStartRecord); + this.listener.Write(this.activityCompleteRecord); } [Benchmark] public void FailedQuery() { // Simulate a failed query execution - this.traceListener!.Write(this.requestStartRecord); - this.metricListener!.Write(this.requestStartRecord); - - this.traceListener.Write(this.exceptionRecord); - - this.traceListener.Write(this.activityCompleteRecord); - this.metricListener.Write(this.activityCompleteRecord); + this.listener!.Write(this.requestStartRecord); + this.listener.Write(this.exceptionRecord); + this.listener.Write(this.activityCompleteRecord); } [Benchmark] public void TraceListenerOnly() { - // Benchmark just the trace listener - this.traceListener!.Write(this.requestStartRecord); - this.traceListener.Write(this.activityCompleteRecord); + // Benchmark just the trace listener (metrics disabled) + this.metricHandle?.Dispose(); + this.listener!.Write(this.requestStartRecord); + this.listener.Write(this.activityCompleteRecord); + this.metricHandle = KustoInstrumentation.HandleManager.AddMetricHandle(); } [Benchmark] public void MetricListenerOnly() { - // Benchmark just the metric listener - this.metricListener!.Write(this.requestStartRecord); - this.metricListener.Write(this.activityCompleteRecord); + // Benchmark just the metric listener (tracing disabled) + this.tracingHandle?.Dispose(); + this.listener!.Write(this.requestStartRecord); + this.listener.Write(this.activityCompleteRecord); + this.tracingHandle = KustoInstrumentation.HandleManager.AddTracingHandle(); } private static KustoUtils.TraceRecord CreateRequestStartRecord(Guid activityId, string clientRequestId, string queryText) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index ac35b3a491..4c968103d3 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -6,6 +6,9 @@ Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ + { + error.type: Kusto.Data.Exceptions.SemanticException + }, { db.system.name: azure.kusto }, @@ -26,9 +29,6 @@ }, { db.namespace: NetDefaultDB - }, - { - error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, @@ -41,12 +41,6 @@ Description: Duration of database client operations, Unit: s, Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative } ], Exception: { diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 7b3f5aeb0b..9e5da67b86 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -6,6 +6,9 @@ Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ + { + error.type: Kusto.Data.Exceptions.SemanticException + }, { db.system.name: azure.kusto }, @@ -32,9 +35,6 @@ }, { db.query.summary: InvalidTable | take - }, - { - error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, @@ -47,12 +47,6 @@ Description: Duration of database client operations, Unit: s, Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative } ], Exception: { diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index f2df19131a..75a8299433 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -37,12 +37,6 @@ Description: Duration of database client operations, Unit: s, Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative } ] } \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt index 274fae0468..17f52c315a 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt @@ -43,12 +43,6 @@ Description: Duration of database client operations, Unit: s, Temporality: Cumulative - }, - { - Name: db.client.operation.count, - Description: Number of database client operations, - Unit: , - Temporality: Cumulative } ] } \ No newline at end of file From dd15076dd8e55be76eb39fbbba9a41c81fda6228 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 12:30:33 -0800 Subject: [PATCH 35/46] Remove url.full attribute which is against spec (and will be covered by HttpClient) --- .../Implementation/KustoTraceRecordListener.cs | 7 +------ ...nvalidTable - take 10_processQuery=False.verified.txt | 9 +++------ ...InvalidTable - take 10_processQuery=True.verified.txt | 9 +++------ ...query=print number=42_processQuery=False.verified.txt | 7 ++----- ..._query=print number=42_processQuery=True.verified.txt | 7 ++----- 5 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index a1775688f3..ffb19f4f56 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -115,18 +115,13 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) if (ShouldComputeTags(activity)) { activity?.DisplayName = operationName; + activity?.AddTag(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); tagList.Add(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); - tagList.Add(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); tagList.Add(SemanticConventions.AttributeDbOperationName, operationName); var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); - if (!string.IsNullOrEmpty(result.Uri)) - { - tagList.Add(SemanticConventions.AttributeUrlFull, result.Uri); - } - if (!string.IsNullOrEmpty(result.ServerAddress)) { tagList.Add(SemanticConventions.AttributeServerAddress, result.ServerAddress); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index 4c968103d3..dace257e99 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -7,20 +7,17 @@ StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ { - error.type: Kusto.Data.Exceptions.SemanticException + kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, { - db.system.name: azure.kusto + error.type: Kusto.Data.Exceptions.SemanticException }, { - kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + db.system.name: azure.kusto }, { db.operation.name: KD.RestClient.ExecuteQuery }, - { - url.full: http://Scrubbed:Scrubbed/v1/rest/query - }, { server.address: Scrubbed }, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 9e5da67b86..7ccd6efcbb 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -7,20 +7,17 @@ StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ { - error.type: Kusto.Data.Exceptions.SemanticException + kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, { - db.system.name: azure.kusto + error.type: Kusto.Data.Exceptions.SemanticException }, { - kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + db.system.name: azure.kusto }, { db.operation.name: KD.RestClient.ExecuteQuery }, - { - url.full: http://Scrubbed:Scrubbed/v1/rest/query - }, { server.address: Scrubbed }, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index 75a8299433..24ddb6e11e 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -5,17 +5,14 @@ Name: Kusto.Client, Status: Ok, TagObjects: [ - { - db.system.name: azure.kusto - }, { kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy }, { - db.operation.name: KD.RestClient.ExecuteQuery + db.system.name: azure.kusto }, { - url.full: http://Scrubbed:Scrubbed/v1/rest/query + db.operation.name: KD.RestClient.ExecuteQuery }, { server.address: Scrubbed diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt index 17f52c315a..2f87a135bc 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt @@ -5,17 +5,14 @@ Name: Kusto.Client, Status: Ok, TagObjects: [ - { - db.system.name: azure.kusto - }, { kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy }, { - db.operation.name: KD.RestClient.ExecuteQuery + db.system.name: azure.kusto }, { - url.full: http://Scrubbed:Scrubbed/v1/rest/query + db.operation.name: KD.RestClient.ExecuteQuery }, { server.address: Scrubbed From 5ea3578894bc5e4e7340bdf7aa1b0711879f213d Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 14:06:07 -0800 Subject: [PATCH 36/46] Fix instrumentation benchmarks --- .../InstrumentationBenchmarks.cs | 22 ++++++++++--------- .../Program.cs | 5 +++-- .../README.md | 20 ++++++++--------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs index fa09d4ad1b..d86b690381 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/InstrumentationBenchmarks.cs @@ -16,8 +16,6 @@ namespace OpenTelemetry.Instrumentation.Kusto.Benchmarks; [MemoryDiagnoser] public class InstrumentationBenchmarks { - private static readonly KustoUtils.ActivityType TestActivityType = new FakeActivtyType(); - private readonly Guid activityId = Guid.NewGuid(); private readonly string clientRequestId = "SW52YWxpZFRhYmxlIHwgdGFrZSAxMA=="; @@ -114,7 +112,9 @@ public void MetricListenerOnly() private static KustoUtils.TraceRecord CreateRequestStartRecord(Guid activityId, string clientRequestId, string queryText) { var message = $$"""$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/query, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\benchmarkuser, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId={{clientRequestId}}, text={{queryText}}"""; - using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + var activity = CreateActivity(activityId, clientRequestId); + using var context = KustoUtils.Context.PushActivityContext(activity); return KustoUtils.TraceRecord.Create("Kusto.Data", KustoUtils.TraceVerbosity.Verbose, message); } @@ -122,7 +122,9 @@ private static KustoUtils.TraceRecord CreateRequestStartRecord(Guid activityId, private static KustoUtils.TraceRecord CreateActivityCompleteRecord(Guid activityId, string clientRequestId) { const string message = "MonitoredActivityCompletedSuccessfully: TestActivityType=KD.RestClient.ExecuteQuery, Timestamp=2025-12-01T02:30:30.0211167Z, ParentActivityId={0}, Duration=4316.802 [ms], HowEnded=Success"; - using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + var activity = CreateActivity(activityId, clientRequestId); + using var context = KustoUtils.Context.PushActivityContext(activity); return KustoUtils.TraceRecord.Create("Kusto.Data", KustoUtils.TraceVerbosity.Verbose, message); } @@ -143,16 +145,16 @@ private static KustoUtils.TraceRecord CreateExceptionRecord(Guid activityId, str DataSource=http://127.0.0.1:62413/v1/rest/query DatabaseName=NetDefaultDB """; - using var context = KustoUtils.Context.PushNewActivityContext(TestActivityType, clientRequestId); + + var activity = CreateActivity(activityId, clientRequestId); + using var context = KustoUtils.Context.PushActivityContext(activity); return KustoUtils.TraceRecord.Create("KD.Exceptions", KustoUtils.TraceVerbosity.Error, message); } - private class FakeActivtyType : KustoUtils.ActivityType + private static KustoUtils.Activity CreateActivity(Guid activityId, string clientRequestId) { - public FakeActivtyType() - : base("FakeActivity", "A fake activity", KustoUtils.TraceVerbosity.Info, KustoUtils.TraceVerbosity.Info, KustoUtils.TraceVerbosity.Info) - { - } + var sub = Guid.NewGuid(); + return KustoUtils.Activity.CreateImportActivity(activityId, sub, sub, clientRequestId, "FakeActivity"); } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs index ea2e249c65..c1bcf5834b 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/Program.cs @@ -12,8 +12,9 @@ private static void Main(string[] args) { if (Debugger.IsAttached) { - var benchmarks = new KustoProcessorProfilingBenchmarks(); - benchmarks.ProcessSummarizeAndSanitize(); + var benchmarks = new InstrumentationBenchmarks(); + benchmarks.Setup(); + benchmarks.FailedQuery(); } else { diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 8b30ffeb5f..073dad000d 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -22,20 +22,20 @@ option number from the list of options shown on the Console window. ``` -BenchmarkDotNet v0.15.6, Windows 11 (10.0.26100.7092/24H2/2024Update/HudsonValley) -Intel Core i9-10940X CPU 3.30GHz (Max: 3.31GHz), 1 CPU, 28 logical and 14 physical cores +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7093) +Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores .NET SDK 10.0.100 - [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 - DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|------------------- |-------------:|-----------:|-----------:|-------:|-------:|----------:| -| SuccessfulQuery | 12,866.04 ns | 252.865 ns | 378.477 ns | 1.1597 | 0.0153 | 11792 B | -| FailedQuery | 13,736.88 ns | 271.636 ns | 453.842 ns | 1.1902 | 0.0153 | 11984 B | -| TraceListenerOnly | 13,281.18 ns | 261.834 ns | 311.695 ns | 1.1444 | 0.0153 | 11592 B | -| MetricListenerOnly | 88.40 ns | 1.783 ns | 2.318 ns | 0.0095 | - | 96 B | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------- |----------:|----------:|----------:|-------:|-------:|----------:| +| SuccessfulQuery | 9.043 μs | 0.1183 μs | 0.1048 μs | 0.9308 | 0.0153 | 11.48 KB | +| FailedQuery | 10.076 μs | 0.2007 μs | 0.3354 μs | 0.9613 | 0.0153 | 11.91 KB | +| TraceListenerOnly | 9.411 μs | 0.1788 μs | 0.2325 μs | 0.9308 | 0.0153 | 11.52 KB | +| MetricListenerOnly | 9.352 μs | 0.1746 μs | 0.2613 μs | 0.9308 | 0.0153 | 11.52 KB | ### Summarization and sanitization processing From 6facd6a9f76e69079f4041c92e62f50f658ad584 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 15:19:39 -0800 Subject: [PATCH 37/46] Clean up registration --- .../Implementation/KustoInstrumentation.cs | 8 +--- .../KustoTraceRecordListener.cs | 6 +-- .../KustoInstrumentationOptions.cs | 2 - .../MeterProviderBuilderExtensions.cs | 37 +++---------------- ...OpenTelemetry.Instrumentation.Kusto.csproj | 2 - .../TracerProviderBuilderExtensions.cs | 37 +++---------------- 6 files changed, 15 insertions(+), 77 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs index da0de699e6..0617942c3f 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs @@ -17,13 +17,9 @@ internal static class KustoInstrumentation return listener; }); - public static KustoInstrumentationOptions TracingOptions { get; set; } = new KustoInstrumentationOptions(); - - public static KustoInstrumentationOptions MetricOptions { get; set; } = new KustoInstrumentationOptions(); + public static KustoInstrumentationOptions Options { get; } = new KustoInstrumentationOptions(); public static InstrumentationHandleManager HandleManager { get; } = new InstrumentationHandleManager(); - public static void InitializeTracing() => _ = Listener.Value; - - public static void InitializeMetrics() => _ = Listener.Value; + public static void Initialize() => _ = Listener.Value; } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index ffb19f4f56..6be50d167d 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -73,7 +73,7 @@ private void CallEnrichment(KustoUtils.TraceRecord record) var activity = this.GetContext(record)?.Activity; if (activity is not null && activity.IsAllDataRequested) { - KustoInstrumentation.TracingOptions.Enrich?.Invoke(activity, record); + KustoInstrumentation.Options.Enrich?.Invoke(activity, record); } } catch (Exception ex) @@ -139,9 +139,9 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) if (!result.QueryText.IsEmpty) { - var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.TracingOptions.RecordQuerySummary, shouldSanitize: KustoInstrumentation.TracingOptions.RecordQueryText, result.QueryText.ToString()); + var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.Options.RecordQuerySummary, shouldSanitize: KustoInstrumentation.Options.RecordQueryText, result.QueryText.ToString()); - if (KustoInstrumentation.TracingOptions.RecordQueryText) + if (KustoInstrumentation.Options.RecordQueryText) { tagList.Add(SemanticConventions.AttributeDbQueryText, info.Sanitized); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs index 5260076ed3..8891c10e74 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs @@ -27,6 +27,4 @@ public class KustoInstrumentationOptions /// Gets or sets an action to enrich the Activity with additional information from the TraceRecord. /// public Action? Enrich { get; set; } - - // TODO: Add flag for query parameter tracing } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs index 70d19599ec..3e6b34db99 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; @@ -19,8 +17,8 @@ public static class MeterProviderBuilderExtensions /// /// being configured. /// The instance of to chain the calls. - public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) - => AddKustoInstrumentation(builder, options => { }); + public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) => + builder.AddKustoInstrumentation(options => { }); /// /// Enables Kusto instrumentation. @@ -29,40 +27,15 @@ public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBui /// Action to configure the . /// The instance of to chain the calls. public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder, Action configureKustoInstrumentationOptions) - { - Guard.ThrowIfNull(configureKustoInstrumentationOptions); - - return AddKustoInstrumentation(builder, name: null, configureKustoInstrumentationOptions); - } - - // TODO: Revisit named options - - /// - /// Enables Kusto instrumentation. - /// - /// being configured. - /// The name of the options instance being configured. - /// Kusto instrumentation options. - /// The instance of to chain the calls. - private static MeterProviderBuilder AddKustoInstrumentation( - this MeterProviderBuilder builder, - string? name, - Action? configureOptions) { Guard.ThrowIfNull(builder); - name ??= Options.DefaultName; + Guard.ThrowIfNull(configureKustoInstrumentationOptions); - if (configureOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureOptions)); - } + configureKustoInstrumentationOptions(KustoInstrumentation.Options); builder.AddInstrumentation(sp => { - var options = sp.GetRequiredService>().Get(name); - KustoInstrumentation.MetricOptions = options; - - KustoInstrumentation.InitializeMetrics(); + KustoInstrumentation.Initialize(); return KustoInstrumentation.HandleManager.AddMetricHandle(); }); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index dae6d0b5ca..0e6035fcd8 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -24,8 +24,6 @@ - - diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 5dd4814a99..4dc22c0739 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; @@ -19,8 +17,8 @@ public static class TracerProviderBuilderExtensions /// /// being configured. /// The instance of to chain the calls. - public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) - => AddKustoInstrumentation(builder, options => { }); + public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) => + AddKustoInstrumentation(builder, options => { }); /// /// Enables Kusto instrumentation. @@ -31,40 +29,15 @@ public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderB public static TracerProviderBuilder AddKustoInstrumentation( this TracerProviderBuilder builder, Action configureKustoInstrumentationOptions) - { - Guard.ThrowIfNull(configureKustoInstrumentationOptions); - return AddKustoInstrumentation(builder, name: null, configureKustoInstrumentationOptions); - } - - // TODO: Revisit named options - - /// - /// Enables Kusto instrumentation. - /// - /// being configured. - /// The name of the options instance being configured. - /// Kusto instrumentation options. - /// The instance of to chain the calls. - private static TracerProviderBuilder AddKustoInstrumentation( - this TracerProviderBuilder builder, - string? name, - Action? configureOptions) { Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(configureKustoInstrumentationOptions); - name ??= Options.DefaultName; - - if (configureOptions != null) - { - builder.ConfigureServices(services => services.Configure(name, configureOptions)); - } + configureKustoInstrumentationOptions(KustoInstrumentation.Options); builder.AddInstrumentation(sp => { - var options = sp.GetRequiredService>().Get(name); - KustoInstrumentation.TracingOptions = options; - - KustoInstrumentation.InitializeTracing(); + KustoInstrumentation.Initialize(); return KustoInstrumentation.HandleManager.AddTracingHandle(); }); From 2e3a3cf85fb6028faefcd0d209e9a58e52ec9c77 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 15:23:01 -0800 Subject: [PATCH 38/46] Clean up package versions --- Directory.Packages.props | 1 + .../OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index feaba01c3d..8a27cd0632 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -126,6 +126,7 @@ + diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj index ed5e041876..2da2366090 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/OpenTelemetry.Instrumentation.Kusto.Benchmarks.csproj @@ -15,7 +15,7 @@ - + From efa14dcc76261a69c1d0ffb825a2bdf28fa0b856 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 16:17:44 -0800 Subject: [PATCH 39/46] Add comments and remove dead code --- .../Implementation/ActivityExtensions.cs | 18 +++++++++ .../InstrumentationHandleManagerExtensions.cs | 17 +++++++++ .../Implementation/KustoInstrumentation.cs | 9 +++++ .../Implementation/KustoProcessor.cs | 37 ++++++++++++++++++- .../KustoTraceRecordListener.cs | 7 ++++ .../Implementation/SpanExtensions.cs | 21 ++++++++--- .../Implementation/StringBuilderExtensions.cs | 34 ----------------- .../Implementation/TraceRecordParser.cs | 9 +++-- .../TraceRecordParserTests.cs | 3 +- 9 files changed, 108 insertions(+), 47 deletions(-) delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs index 3135c32339..e63f301e25 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs @@ -5,8 +5,26 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Extensions on for . +/// internal static class ActivityExtensions { + /// + /// Adds the specified tags to the activity if they do not already exist. + /// + /// + /// This method does not overwrite existing tags on the activity. Only tags with keys not already + /// present are added. + /// + /// The activity to which tags will be added. + /// + /// The collection of tags to add to the activity. Each tag is added only if its key does not already exist on the + /// activity. + /// + /// + /// The activity instance with the new tags added, or unchanged if all tag keys already exist. + /// public static Activity AddTags(this Activity activity, TagList tags) { foreach (var tag in tags) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs index 3d9df2db91..d7702198d8 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs @@ -3,13 +3,30 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Provides extension methods for . +/// internal static class InstrumentationHandleManagerExtensions { + /// + /// Returns if tracing is active (i.e., there is at least one tracing handle); otherwise, . + /// + /// + /// The to check for active tracing handles. + /// + /// if tracing is active; otherwise, . public static bool IsTracingActive(this InstrumentationHandleManager handleManager) { return handleManager.TracingHandles > 0; } + /// + /// Returns if metrics is active (i.e., there is at least one metrics handle); otherwise, . + /// + /// + /// The to check for active metrics handles. + /// + /// if metrics is active; otherwise, . public static bool IsMetricsActive(this InstrumentationHandleManager handleManager) { return handleManager.MetricHandles > 0; diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs index 0617942c3f..091d7e3d03 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs @@ -5,6 +5,9 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Class to hold the singleton instances used for Kusto instrumentation. +/// internal static class KustoInstrumentation { private static readonly Lazy Listener = new(() => @@ -17,8 +20,14 @@ internal static class KustoInstrumentation return listener; }); + /// + /// Gets the post-configured options for Kusto instrumentation. + /// public static KustoInstrumentationOptions Options { get; } = new KustoInstrumentationOptions(); + /// + /// Gets the that tracks if there are any active listeners for . + /// public static InstrumentationHandleManager HandleManager { get; } = new InstrumentationHandleManager(); public static void Initialize() => _ = Listener.Value; diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs index 7e7f67aa91..1599369529 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoProcessor.cs @@ -8,8 +8,12 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Use the Kusto query language services to process Kusto queries for summarization and sanitization. +/// internal static class KustoProcessor { + // Because we're not doing full semantic analysis for queries, we can reuse the default global state (which includes all built-in functions and types) private static readonly GlobalState KustoParserGlobalState = GlobalState.Default.WithCache(); private enum ReplacementKind @@ -18,6 +22,30 @@ private enum ReplacementKind Remove, } + /// + /// Processes the specified Kusto query and optionally generates a summary and/or a sanitized version based on the + /// provided options. + /// + /// + /// If both summarization and sanitization are requested, the query is parsed only once for + /// efficiency. The returned will have null values for summary or sanitized output + /// if the corresponding option is not enabled. + /// + /// + /// Indicates whether to generate a summary of the query. If , the returned object will + /// include a summarized representation. + /// + /// + /// Indicates whether to generate a sanitized version of the query. If , the returned object + /// will include a sanitized representation. + /// + /// + /// The Kusto query to process. + /// + /// + /// A containing the summary and/or sanitized version of the query, depending on + /// the options specified. + /// public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSanitize, string query) { string? summarized = null; @@ -25,7 +53,8 @@ public static KustoStatementInfo Process(bool shouldSummarize, bool shouldSaniti KustoCode? code = null; - // Note that order matters here as summarization requires semantic analysis, but we want to avoid parsing twice if both are requested. + // Note that order matters here as summarization requires semantic analysis to find potential table references, + // but we want to avoid parsing twice if both are requested. if (shouldSummarize) { code ??= KustoCode.ParseAndAnalyze(query, KustoParserGlobalState); @@ -75,6 +104,9 @@ private static string Summarize(KustoCode code) private static TextEdit CreateRemoval(SyntaxElement node) => TextEdit.Deletion(node.TextStart, node.Width); + /// + /// Visitor that traverses the KQL looking for literal values to replace with the PLACEHOLDER value. + /// private sealed class SanitizerVisitor : DefaultSyntaxVisitor { private readonly List edits = []; @@ -119,6 +151,9 @@ private void VisitChildren(SyntaxNode node) } } + /// + /// Visitor that traverses the KQL to produce a summarized representation of the query. + /// private sealed class SummarizerVisitor : DefaultSyntaxVisitor, IDisposable { private readonly TruncatingStringBuilder builder = new(); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index 6be50d167d..6eb567c889 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -8,6 +8,13 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Class that is registered with the Kusto client library to receive trace records. +/// +/// +/// The Kusto client library uses its own tracing infrastructure. Many types share names with common diagnostic types +/// (e.g. Activity, ITraceListener, etc.) but in the Kusto.Cloud.Platform.Utils namespace. +/// internal sealed class KustoTraceRecordListener : KustoUtils.ITraceListener { // The client's async machinery may not call us back using the same AsyncLocal context, so we must manually track diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs index dd0fdc31b7..1f1cdc9969 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/SpanExtensions.cs @@ -3,17 +3,26 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Extension methods for . +/// internal static class SpanExtensions { + /// + /// Slices the span after the first occurrence of the . + /// + /// + /// The span to slice. + /// + /// + /// The value to search for. + /// + /// + /// A that is a slice of the original span after the first occurrence of the . + /// public static ReadOnlySpan SliceAfter(this ReadOnlySpan span, ReadOnlySpan needle) { var idx = span.IndexOf(needle); return idx >= 0 ? span.Slice(idx + needle.Length) : []; } - - public static ReadOnlySpan SliceBefore(this ReadOnlySpan span, ReadOnlySpan needle) - { - var idx = span.IndexOf(needle); - return idx >= 0 ? span.Slice(0, idx) : []; - } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs deleted file mode 100644 index f6fd8eeae4..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/StringBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Text; - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -internal static class StringBuilderExtensions -{ - public static StringBuilder TrimEnd(this StringBuilder sb) - { - if (sb.Length == 0) - { - return sb; - } - - int i = sb.Length - 1; - - for (; i >= 0; i--) - { - if (!char.IsWhiteSpace(sb[i])) - { - break; - } - } - - if (i < sb.Length - 1) - { - sb.Length = i + 1; - } - - return sb; - } -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs index 304569b905..b9ba03a477 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/TraceRecordParser.cs @@ -7,6 +7,9 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; +/// +/// Class that parses the delimited messages in instances. +/// internal class TraceRecordParser { #if NET9_0_OR_GREATER @@ -25,7 +28,7 @@ public static ParsedRequestStart ParseRequestStart(ReadOnlySpan message) // so we can just take everything after "text=" var queryText = message.SliceAfter("text="); - return new ParsedRequestStart(uri, parsed?.Host, parsed?.Port, database, queryText); + return new ParsedRequestStart(parsed?.Host, parsed?.Port, database, queryText); } public static ParsedActivityComplete ParseActivityComplete(ReadOnlySpan message) @@ -59,15 +62,13 @@ private static ReadOnlySpan ExtractValueBetween(ReadOnlySpan haystac internal readonly ref struct ParsedRequestStart { - public readonly string Uri; public readonly string? ServerAddress; public readonly int? ServerPort; public readonly ReadOnlySpan Database; public readonly ReadOnlySpan QueryText; - public ParsedRequestStart(string uri, string? serverAddress, int? serverPort, ReadOnlySpan database, ReadOnlySpan queryText) + public ParsedRequestStart(string? serverAddress, int? serverPort, ReadOnlySpan database, ReadOnlySpan queryText) { - this.Uri = uri; this.ServerAddress = serverAddress; this.ServerPort = serverPort; this.Database = database; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs index 4cb53652b6..c4bed986be 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/TraceRecordParserTests.cs @@ -14,7 +14,6 @@ public void ParseRequestStartSuccess() const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://127.0.0.1:49902/v1/rest/message, DatabaseName=NetDefaultDB, App=testhost, User=REDMOND\\mattkot, ClientVersion=Kusto.Dotnet.Client:{14.0.2+b2d66614da1a4ff4561c5037c48e5be7002d66d4}|Runtime:{.NET_10.0.0/CLRv10.0.0/10.0.0-rtm.25523.111}, ClientRequestId=SW52YWxpZFRhYmxlIHwgdGFrZSAxMCB8IHdoZXJlIENvbDEgPSA3, text=InvalidTable | take 10 | where Col1=7 | summarize by Date, Time"; var result = TraceRecordParser.ParseRequestStart(message); - Assert.Equal("http://127.0.0.1:49902/v1/rest/message", result.Uri); Assert.Equal("127.0.0.1", result.ServerAddress); Assert.Equal("49902", result.ServerPort.ToString()); Assert.Equal("NetDefaultDB", result.Database.ToString()); @@ -27,7 +26,6 @@ public void ParseRequestStartFailure() const string message = "$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://"; var result = TraceRecordParser.ParseRequestStart(message); - Assert.Equal("http://", result.Uri); Assert.Null(result.ServerAddress); Assert.Equal(string.Empty, result.ServerPort.ToString()); Assert.Equal(string.Empty, result.Database.ToString()); @@ -35,6 +33,7 @@ public void ParseRequestStartFailure() } [Theory] + [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=https://clustername.kusto.windows.net/v1/rest/query, DatabaseName=TestDB, text=print 1", "clustername.kusto.windows.net", 443)] [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://localhost/v1/rest/query, DatabaseName=TestDB, text=print 1", "localhost", 80)] [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=http://[2001:db8::1]:8080/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", 8080)] [InlineData("$$HTTPREQUEST[RestClient2]: Verb=POST, Uri=https://[2001:db8::1]/v1/rest/query, DatabaseName=TestDB, text=print 1", "[2001:db8::1]", 443)] From 42cbf916e66f417794e2abbbd9dae4a8e705ac77 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 16:40:19 -0800 Subject: [PATCH 40/46] Add metrics-only and trace-only integration tests --- .../KustoTraceRecordListener.cs | 24 ++- .../KustoIntegrationTests.cs | 163 +++++++++++------- ...nlyTest_query=print number=42.verified.txt | 10 ++ ...nlyTest_query=print number=42.verified.txt | 34 ++++ 4 files changed, 166 insertions(+), 65 deletions(-) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index 6eb567c889..378ef72d85 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -180,12 +180,12 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) var result = TraceRecordParser.ParseActivityComplete(record.Message.AsSpan()); if (result.HowEnded.Equals("Success".AsSpan(), StringComparison.Ordinal)) { - activity.SetStatus(ActivityStatusCode.Ok); + activity?.SetStatus(ActivityStatusCode.Ok); } - activity.AddTags(context.Value.Tags); + activity?.AddTags(context.Value.Tags); this.CallEnrichment(record); - activity.Stop(); + activity?.Stop(); var duration = activity?.Duration.TotalSeconds ?? GetElapsedTime(context.Value.BeginTimestamp); KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, context.Value.Tags); @@ -203,6 +203,9 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) return null; } + /// + /// Holds context data for an ongoing operation. + /// private readonly struct ContextData { public ContextData(long beginTimestamp, TagList tags, Activity activity) @@ -212,10 +215,23 @@ public ContextData(long beginTimestamp, TagList tags, Activity activity) this.Activity = activity; } + /// + /// Gets the timestamp when the operation began. Used to compute duration if the + /// is not available (i.e. in a metrics-only scenario). + /// public long BeginTimestamp { get; } + /// + /// Gets the collection of tags associated with the operation that should be shared between the span and metrics. + /// public TagList Tags { get; } - public Activity Activity { get; } + /// + /// Gets the current activity associated with the instance, if any. + /// + /// + /// Will be in a metrics-only scenario. + /// + public Activity? Activity { get; } } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 4f89569034..36b3fcd122 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -61,40 +61,88 @@ public async Task SuccessfulQueryTest(string query, bool processQuery) tracerProvider.ForceFlush(); meterProvider.ForceFlush(); - var activitySnapshots = activities - .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) - .Select(activity => new + await Verify( + new { - activity.DisplayName, - activity.Source.Name, - activity.Status, - activity.StatusDescription, - activity.TagObjects, - activity.OperationName, - activity.IdFormat, - }); + Activities = FilterActivites(activities), + Metrics = FilterMetrics(metrics), + }) + .ScrubHostname(kcsb.Hostname) + .ScrubPort(this.fixture.DatabaseContainer.GetMappedPublicPort()) + .UseDirectory("Snapshots") + .UseParameters(query, processQuery); + } - var metricSnapshots = metrics - .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) - .Select(metric => new + [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] + [InlineData("print number=42")] + public async Task TraceOnlyTest(string query) + { + var activities = new List(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddInMemoryExporter(activities) + .AddKustoInstrumentation() + .Build(); + + var kcsb = this.fixture.ConnectionStringBuilder; + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + // Ensure a stable client ID for snapshots + ClientRequestId = Convert.ToBase64String(Encoding.UTF8.GetBytes(query)), + }; + + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + reader.Consume(); + + tracerProvider.ForceFlush(); + + await Verify( + new { - metric.Name, - metric.Description, - metric.MeterTags, - metric.Unit, - metric.Temporality, - }); + Activities = FilterActivites(activities), + }) + .ScrubHostname(kcsb.Hostname) + .ScrubPort(this.fixture.DatabaseContainer.GetMappedPublicPort()) + .UseDirectory("Snapshots") + .UseParameters(query); + } + + [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] + [InlineData("print number=42")] + public async Task MetricsOnlyTest(string query) + { + var metrics = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(metrics) + .AddKustoInstrumentation() + .Build(); + + var kcsb = this.fixture.ConnectionStringBuilder; + using var queryProvider = KustoClientFactory.CreateCslQueryProvider(kcsb); + + var crp = new ClientRequestProperties() + { + // Ensure a stable client ID for snapshots + ClientRequestId = Convert.ToBase64String(Encoding.UTF8.GetBytes(query)), + }; + + using var reader = queryProvider.ExecuteQuery("NetDefaultDB", query, crp); + reader.Consume(); + + meterProvider.ForceFlush(); await Verify( new { - Activities = activitySnapshots, - Metrics = metricSnapshots, + Metrics = FilterMetrics(metrics), }) .ScrubHostname(kcsb.Hostname) .ScrubPort(this.fixture.DatabaseContainer.GetMappedPublicPort()) .UseDirectory("Snapshots") - .UseParameters(query, processQuery); + .UseParameters(query); } [EnabledOnDockerPlatformTheory(DockerPlatform.Linux)] @@ -140,35 +188,11 @@ public async Task FailedQueryTest(string query, bool processQuery) tracerProvider.ForceFlush(); meterProvider.ForceFlush(); - var activitySnapshots = activities - .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) - .Select(activity => new - { - activity.DisplayName, - activity.Source.Name, - activity.Status, - activity.StatusDescription, - activity.TagObjects, - activity.OperationName, - activity.IdFormat, - }); - - var metricSnapshots = metrics - .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) - .Select(metric => new - { - metric.Name, - metric.Description, - metric.MeterTags, - metric.Unit, - metric.Temporality, - }); - await Verify( new { - Activities = activitySnapshots, - Metrics = metricSnapshots, + Activities = FilterActivites(activities), + Metrics = FilterMetrics(metrics), Exception = new { Type = exception.GetType().FullName, @@ -212,17 +236,8 @@ public void NoInstrumentationRegistered_NoEventsEmitted() tracerProvider.ForceFlush(); meterProvider.ForceFlush(); - // Assert - No Kusto activities or metrics should be emitted - var kustoActivities = activities - .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) - .ToList(); - - var kustoMetrics = metrics - .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) - .ToList(); - - Assert.Empty(kustoActivities); - Assert.Empty(kustoMetrics); + Assert.Empty(FilterActivites(activities)); + Assert.Empty(FilterMetrics(metrics)); } [EnabledOnDockerPlatformFact(DockerPlatform.Linux)] @@ -293,11 +308,37 @@ public void EnrichCallbackTest() var activity = kustoActivities[0]; // Verify the custom summary was set by the Enrich callback - var querySummaryTag = activity.Tags.SingleOrDefault(t => t.Key == SemanticConventions.AttributeDbQuerySummary); + var querySummaryTag = activity.TagObjects.SingleOrDefault(t => t.Key == SemanticConventions.AttributeDbQuerySummary); Assert.NotNull(querySummaryTag.Key); Assert.Equal(summary, querySummaryTag.Value); // Verify the display name was set to the custom summary Assert.Equal(summary, activity.DisplayName); } + + private static dynamic FilterActivites(IEnumerable activities) => + activities + .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) + .Select(activity => new + { + activity.DisplayName, + activity.Source.Name, + activity.Status, + activity.StatusDescription, + activity.TagObjects, + activity.OperationName, + activity.IdFormat, + }); + + private static dynamic FilterMetrics(IEnumerable metrics) => + metrics + .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) + .Select(metric => new + { + metric.Name, + metric.Description, + metric.MeterTags, + metric.Unit, + metric.Temporality, + }); } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt new file mode 100644 index 0000000000..3be0840613 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt @@ -0,0 +1,10 @@ +{ + Metrics: [ + { + Name: db.client.operation.duration, + Description: Duration of database client operations, + Unit: s, + Temporality: Cumulative + } + ] +} \ No newline at end of file diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt new file mode 100644 index 0000000000..ca25b7d6a8 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt @@ -0,0 +1,34 @@ +{ + Activities: [ + { + DisplayName: KD.RestClient.ExecuteQuery, + Name: Kusto.Client, + Status: Ok, + TagObjects: [ + { + kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + }, + { + db.system.name: azure.kusto + }, + { + db.operation.name: KD.RestClient.ExecuteQuery + }, + { + server.address: Scrubbed + }, + { + server.port: Scrubbed + }, + { + db.namespace: NetDefaultDB + }, + { + db.query.text: print number=? + } + ], + OperationName: KD.RestClient.ExecuteQuery, + IdFormat: W3C + } + ] +} \ No newline at end of file From 8ad211e5d810117bc619b303700ba98d771d7e4a Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Fri, 5 Dec 2025 16:58:27 -0800 Subject: [PATCH 41/46] Add additional logging for errors --- .../KustoInstrumentationEventSource.cs | 27 +++++++++++++++++++ .../KustoTraceRecordListener.cs | 25 +++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs index 12e51577af..77aed0f78d 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs @@ -28,4 +28,31 @@ public void EnrichmentException(string exception) { this.WriteEvent(1, exception); } + + [Event(2, Message = "Trace record payload is NULL or has NULL message, record will not be processed.", Level = EventLevel.Warning)] + public void NullPayload() + { + this.WriteEvent(2); + } + + [Event(3, Message = "Failed to find context for activity ID '{0}', operation data will not be recorded.", Level = EventLevel.Warning)] + public void ContextNotFound(string activityId) + { + this.WriteEvent(3, activityId); + } + + [NonEvent] + public void UnknownErrorProcessingTraceRecord(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.UnknownErrorProcessingTraceRecord(ex.ToInvariantString()); + } + } + + [Event(4, Message = "Unknown error processing trace record, Exception: {0}", Level = EventLevel.Error)] + public void UnknownErrorProcessingTraceRecord(string exception) + { + this.WriteEvent(4, exception); + } } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index 378ef72d85..3d25012da0 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -33,6 +33,7 @@ public override void Write(KustoUtils.TraceRecord record) { if (record?.Message is null) { + KustoInstrumentationEventSource.Log.NullPayload(); return; } @@ -41,17 +42,24 @@ public override void Write(KustoUtils.TraceRecord record) return; } - if (record.IsRequestStart()) - { - this.HandleHttpRequestStart(record); - } - else if (record.IsActivityComplete()) + try { - this.HandleActivityComplete(record); + if (record.IsRequestStart()) + { + this.HandleHttpRequestStart(record); + } + else if (record.IsActivityComplete()) + { + this.HandleActivityComplete(record); + } + else if (record.IsException()) + { + this.HandleException(record); + } } - else if (record.IsException()) + catch (Exception ex) { - this.HandleException(record); + KustoInstrumentationEventSource.Log.UnknownErrorProcessingTraceRecord(ex); } } @@ -200,6 +208,7 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) return context; } + KustoInstrumentationEventSource.Log.ContextNotFound(record.Activity.ActivityId.ToString()); return null; } From eb744964ecd2c67ccb723b58c0ce0536be556165 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 8 Dec 2025 09:18:12 -0800 Subject: [PATCH 42/46] Move initialization earlier to ensure it is before any kusto clients are made --- .../MeterProviderBuilderExtensions.cs | 4 +++- .../TracerProviderBuilderExtensions.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs index 3e6b34db99..0f67b1bff8 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs @@ -33,9 +33,11 @@ public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBui configureKustoInstrumentationOptions(KustoInstrumentation.Options); + // Be sure to eagerly initialize the instrumentation, as we must set environment variables before any clients are created. + KustoInstrumentation.Initialize(); + builder.AddInstrumentation(sp => { - KustoInstrumentation.Initialize(); return KustoInstrumentation.HandleManager.AddMetricHandle(); }); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 4dc22c0739..9df41bb9ee 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -35,9 +35,11 @@ public static TracerProviderBuilder AddKustoInstrumentation( configureKustoInstrumentationOptions(KustoInstrumentation.Options); + // Be sure to eagerly initialize the instrumentation, as we must set environment variables before any clients are created. + KustoInstrumentation.Initialize(); + builder.AddInstrumentation(sp => { - KustoInstrumentation.Initialize(); return KustoInstrumentation.HandleManager.AddTracingHandle(); }); From e36e1c4735d27843d3107acbcf9f8c8601b2b9e7 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 8 Dec 2025 09:32:58 -0800 Subject: [PATCH 43/46] Fix naming of ActivitySource and Meter --- .../Implementation/KustoActivitySourceHelper.cs | 9 ++++++--- .../KustoIntegrationTests.cs | 3 ++- ...nvalidTable - take 10_processQuery=False.verified.txt | 5 +++-- ...InvalidTable - take 10_processQuery=True.verified.txt | 5 +++-- ...ts.MetricsOnlyTest_query=print number=42.verified.txt | 1 + ...query=print number=42_processQuery=False.verified.txt | 5 +++-- ..._query=print number=42_processQuery=True.verified.txt | 5 +++-- ...ests.TraceOnlyTest_query=print number=42.verified.txt | 4 ++-- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs index 0ffc2af507..b1c6cdb0e2 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoActivitySourceHelper.cs @@ -14,13 +14,16 @@ namespace OpenTelemetry.Instrumentation.Kusto.Implementation; internal static class KustoActivitySourceHelper { public const string DbSystem = "azure.kusto"; - public const string ActivitySourceName = "Kusto.Client"; - public const string MeterName = "Kusto.Client"; - public const string ClientRequestIdTagKey = "kusto.client_request_id"; + public const string ClientRequestIdTagKey = $"{DbSystem}.client_request_id"; public static readonly Assembly Assembly = typeof(KustoActivitySourceHelper).Assembly; + public static readonly AssemblyName AssemblyName = Assembly.GetName(); public static readonly string PackageVersion = Assembly.GetPackageVersion(); + + public static readonly string ActivitySourceName = AssemblyName.Name!; public static readonly ActivitySource ActivitySource = new(ActivitySourceName, PackageVersion); + + public static readonly string MeterName = AssemblyName.Name!; public static readonly Meter Meter = new(MeterName, PackageVersion); public static readonly Histogram OperationDurationHistogram = Meter.CreateHistogram( diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 36b3fcd122..9cf92c55a9 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -321,8 +321,8 @@ private static dynamic FilterActivites(IEnumerable activities) => .Where(activity => activity.Source == KustoActivitySourceHelper.ActivitySource) .Select(activity => new { + ActivitySourceName = activity.Source.Name, activity.DisplayName, - activity.Source.Name, activity.Status, activity.StatusDescription, activity.TagObjects, @@ -335,6 +335,7 @@ private static dynamic FilterMetrics(IEnumerable metrics) => .Where(metric => metric.MeterName == KustoActivitySourceHelper.MeterName) .Select(metric => new { + metric.MeterName, metric.Name, metric.Description, metric.MeterTags, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index dace257e99..02a39c89b0 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -1,13 +1,13 @@ { Activities: [ { + ActivitySourceName: OpenTelemetry.Instrumentation.Kusto, DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ { - kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + azure.kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, { error.type: Kusto.Data.Exceptions.SemanticException @@ -34,6 +34,7 @@ ], Metrics: [ { + MeterName: OpenTelemetry.Instrumentation.Kusto, Name: db.client.operation.duration, Description: Duration of database client operations, Unit: s, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 7ccd6efcbb..518209b047 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -1,13 +1,13 @@ { Activities: [ { + ActivitySourceName: OpenTelemetry.Instrumentation.Kusto, DisplayName: InvalidTable | take, - Name: Kusto.Client, Status: Error, StatusDescription: 'take' operator: Failed to resolve table or column expression named 'InvalidTable', TagObjects: [ { - kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== + azure.kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, { error.type: Kusto.Data.Exceptions.SemanticException @@ -40,6 +40,7 @@ ], Metrics: [ { + MeterName: OpenTelemetry.Instrumentation.Kusto, Name: db.client.operation.duration, Description: Duration of database client operations, Unit: s, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt index 3be0840613..d35fa730f3 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.MetricsOnlyTest_query=print number=42.verified.txt @@ -1,6 +1,7 @@ { Metrics: [ { + MeterName: OpenTelemetry.Instrumentation.Kusto, Name: db.client.operation.duration, Description: Duration of database client operations, Unit: s, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt index 24ddb6e11e..cf190459f0 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=False.verified.txt @@ -1,12 +1,12 @@ { Activities: [ { + ActivitySourceName: OpenTelemetry.Instrumentation.Kusto, DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, Status: Ok, TagObjects: [ { - kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + azure.kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy }, { db.system.name: azure.kusto @@ -30,6 +30,7 @@ ], Metrics: [ { + MeterName: OpenTelemetry.Instrumentation.Kusto, Name: db.client.operation.duration, Description: Duration of database client operations, Unit: s, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt index 2f87a135bc..5307354f42 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.SuccessfulQueryTest_query=print number=42_processQuery=True.verified.txt @@ -1,12 +1,12 @@ { Activities: [ { + ActivitySourceName: OpenTelemetry.Instrumentation.Kusto, DisplayName: print, - Name: Kusto.Client, Status: Ok, TagObjects: [ { - kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + azure.kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy }, { db.system.name: azure.kusto @@ -36,6 +36,7 @@ ], Metrics: [ { + MeterName: OpenTelemetry.Instrumentation.Kusto, Name: db.client.operation.duration, Description: Duration of database client operations, Unit: s, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt index ca25b7d6a8..de2c4d774d 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.TraceOnlyTest_query=print number=42.verified.txt @@ -1,12 +1,12 @@ { Activities: [ { + ActivitySourceName: OpenTelemetry.Instrumentation.Kusto, DisplayName: KD.RestClient.ExecuteQuery, - Name: Kusto.Client, Status: Ok, TagObjects: [ { - kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy + azure.kusto.client_request_id: cHJpbnQgbnVtYmVyPTQy }, { db.system.name: azure.kusto From 524f04d9a5cbd3e4cd2a72e2e27bda99e751a012 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 8 Dec 2025 10:39:05 -0800 Subject: [PATCH 44/46] Split InstrumentationOptions for Trace and Meter --- .../.publicApi/PublicAPI.Unshipped.txt | 26 +-- .../Implementation/ActivityExtensions.cs | 40 ----- .../InstrumentationHandleManagerExtensions.cs | 10 +- .../Implementation/KustoInstrumentation.cs | 14 +- .../KustoInstrumentationEventSource.cs | 20 +-- .../KustoTraceRecordListener.cs | 63 ++++--- .../KustoMeterInstrumentationOptions.cs | 22 +++ ...cs => KustoTraceInstrumentationOptions.cs} | 6 +- .../MeterProviderBuilderExtensions.cs | 16 +- ...OpenTelemetry.Instrumentation.Kusto.csproj | 2 + .../TracerProviderBuilderExtensions.cs | 16 +- .../DependencyInjectionConfigTests.cs | 169 ++++++++++++++++++ .../KustoIntegrationTests.cs | 18 +- .../KustoTraceProviderBuilderTests.cs | 17 +- ... - take 10_processQuery=False.verified.txt | 6 +- ...e - take 10_processQuery=True.verified.txt | 6 +- 16 files changed, 317 insertions(+), 134 deletions(-) delete mode 100644 src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.Kusto/KustoMeterInstrumentationOptions.cs rename src/OpenTelemetry.Instrumentation.Kusto/{KustoInstrumentationOptions.cs => KustoTraceInstrumentationOptions.cs} (87%) create mode 100644 test/OpenTelemetry.Instrumentation.Kusto.Tests/DependencyInjectionConfigTests.cs diff --git a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt index 4af47d3f0c..370806f093 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Kusto/.publicApi/PublicAPI.Unshipped.txt @@ -1,15 +1,21 @@ #nullable enable -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.Enrich.get -> System.Action? -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.Enrich.set -> void -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.KustoInstrumentationOptions() -> void -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.get -> bool -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQuerySummary.set -> void -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.get -> bool -OpenTelemetry.Instrumentation.Kusto.KustoInstrumentationOptions.RecordQueryText.set -> void +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions.KustoMeterInstrumentationOptions() -> void +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions.RecordQuerySummary.get -> bool +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions.RecordQuerySummary.set -> void +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions.RecordQueryText.get -> bool +OpenTelemetry.Metrics.KustoMeterInstrumentationOptions.RecordQueryText.set -> void OpenTelemetry.Metrics.MeterProviderBuilderExtensions +OpenTelemetry.Trace.KustoTraceInstrumentationOptions +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.Enrich.get -> System.Action? +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.KustoTraceInstrumentationOptions() -> void +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.RecordQuerySummary.get -> bool +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.RecordQuerySummary.set -> void +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.RecordQueryText.get -> bool +OpenTelemetry.Trace.KustoTraceInstrumentationOptions.RecordQueryText.set -> void OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder) -> OpenTelemetry.Metrics.MeterProviderBuilder! -static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder! +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder! builder, System.Action? configureKustoMeterInstrumentationOptions) -> OpenTelemetry.Metrics.MeterProviderBuilder! static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder) -> OpenTelemetry.Trace.TracerProviderBuilder! -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action! configureKustoInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder! +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddKustoInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder! builder, System.Action? configureKustoTraceInstrumentationOptions) -> OpenTelemetry.Trace.TracerProviderBuilder! diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs deleted file mode 100644 index e63f301e25..0000000000 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/ActivityExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Diagnostics; - -namespace OpenTelemetry.Instrumentation.Kusto.Implementation; - -/// -/// Extensions on for . -/// -internal static class ActivityExtensions -{ - /// - /// Adds the specified tags to the activity if they do not already exist. - /// - /// - /// This method does not overwrite existing tags on the activity. Only tags with keys not already - /// present are added. - /// - /// The activity to which tags will be added. - /// - /// The collection of tags to add to the activity. Each tag is added only if its key does not already exist on the - /// activity. - /// - /// - /// The activity instance with the new tags added, or unchanged if all tag keys already exist. - /// - public static Activity AddTags(this Activity activity, TagList tags) - { - foreach (var tag in tags) - { - if (activity.GetTagItem(tag.Key) is null) - { - activity.AddTag(tag.Key, tag.Value); - } - } - - return activity; - } -} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs index d7702198d8..4a70716fef 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/InstrumentationHandleManagerExtensions.cs @@ -15,10 +15,7 @@ internal static class InstrumentationHandleManagerExtensions /// The to check for active tracing handles. /// /// if tracing is active; otherwise, . - public static bool IsTracingActive(this InstrumentationHandleManager handleManager) - { - return handleManager.TracingHandles > 0; - } + public static bool IsTracingActive(this InstrumentationHandleManager handleManager) => handleManager.TracingHandles > 0; /// /// Returns if metrics is active (i.e., there is at least one metrics handle); otherwise, . @@ -27,8 +24,5 @@ public static bool IsTracingActive(this InstrumentationHandleManager handleManag /// The to check for active metrics handles. /// /// if metrics is active; otherwise, . - public static bool IsMetricsActive(this InstrumentationHandleManager handleManager) - { - return handleManager.MetricHandles > 0; - } + public static bool IsMetricsActive(this InstrumentationHandleManager handleManager) => handleManager.MetricHandles > 0; } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs index 091d7e3d03..002b01519b 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentation.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using Kusto.Cloud.Platform.Utils; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; namespace OpenTelemetry.Instrumentation.Kusto.Implementation; @@ -21,14 +23,22 @@ internal static class KustoInstrumentation }); /// - /// Gets the post-configured options for Kusto instrumentation. + /// Gets or sets the post-configured trace options for Kusto instrumentation. /// - public static KustoInstrumentationOptions Options { get; } = new KustoInstrumentationOptions(); + public static KustoTraceInstrumentationOptions TraceOptions { get; set; } = new KustoTraceInstrumentationOptions(); + + /// + /// Gets or sets the post-configured meter options for Kusto instrumentation. + /// + public static KustoMeterInstrumentationOptions MeterOptions { get; set; } = new KustoMeterInstrumentationOptions(); /// /// Gets the that tracks if there are any active listeners for . /// public static InstrumentationHandleManager HandleManager { get; } = new InstrumentationHandleManager(); + /// + /// Initializes the Kusto instrumentation by ensuring the listener is created and registered with the client library. + /// public static void Initialize() => _ = Listener.Value; } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs index 77aed0f78d..dc36473da8 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoInstrumentationEventSource.cs @@ -24,22 +24,13 @@ public void EnrichmentException(Exception ex) } [Event(1, Message = "Enrichment exception: {0}", Level = EventLevel.Error)] - public void EnrichmentException(string exception) - { - this.WriteEvent(1, exception); - } + public void EnrichmentException(string exception) => this.WriteEvent(1, exception); [Event(2, Message = "Trace record payload is NULL or has NULL message, record will not be processed.", Level = EventLevel.Warning)] - public void NullPayload() - { - this.WriteEvent(2); - } + public void NullPayload() => this.WriteEvent(2); [Event(3, Message = "Failed to find context for activity ID '{0}', operation data will not be recorded.", Level = EventLevel.Warning)] - public void ContextNotFound(string activityId) - { - this.WriteEvent(3, activityId); - } + public void ContextNotFound(string activityId) => this.WriteEvent(3, activityId); [NonEvent] public void UnknownErrorProcessingTraceRecord(Exception ex) @@ -51,8 +42,5 @@ public void UnknownErrorProcessingTraceRecord(Exception ex) } [Event(4, Message = "Unknown error processing trace record, Exception: {0}", Level = EventLevel.Error)] - public void UnknownErrorProcessingTraceRecord(string exception) - { - this.WriteEvent(4, exception); - } + public void UnknownErrorProcessingTraceRecord(string exception) => this.WriteEvent(4, exception); } diff --git a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs index 3d25012da0..c6c49119b0 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/Implementation/KustoTraceRecordListener.cs @@ -88,7 +88,7 @@ private void CallEnrichment(KustoUtils.TraceRecord record) var activity = this.GetContext(record)?.Activity; if (activity is not null && activity.IsAllDataRequested) { - KustoInstrumentation.Options.Enrich?.Invoke(activity, record); + KustoInstrumentation.TraceOptions.Enrich?.Invoke(activity, record); } } catch (Exception ex) @@ -110,7 +110,7 @@ private void HandleException(KustoUtils.TraceRecord record) if (!result.ErrorType.IsEmpty) { activity?.SetTag(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); - context.Value.Tags.Add(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); + context.Value.MeterTags.Add(SemanticConventions.AttributeErrorType, result.ErrorType.ToString()); } var description = result.ErrorMessage.IsEmpty ? null : result.ErrorMessage.ToString(); @@ -125,52 +125,74 @@ private void HandleHttpRequestStart(KustoUtils.TraceRecord record) var operationName = record.Activity.ActivityType; var activity = KustoActivitySourceHelper.ActivitySource.StartActivity(operationName, ActivityKind.Client); - var tagList = default(TagList); + var meterTags = default(TagList); if (ShouldComputeTags(activity)) { activity?.DisplayName = operationName; activity?.AddTag(KustoActivitySourceHelper.ClientRequestIdTagKey, record.Activity.ClientRequestId.ToString()); - tagList.Add(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); - tagList.Add(SemanticConventions.AttributeDbOperationName, operationName); + activity?.AddTag(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); + activity?.AddTag(SemanticConventions.AttributeDbOperationName, operationName); + meterTags.Add(SemanticConventions.AttributeDbSystemName, KustoActivitySourceHelper.DbSystem); + meterTags.Add(SemanticConventions.AttributeDbOperationName, operationName); var result = TraceRecordParser.ParseRequestStart(record.Message.AsSpan()); if (!string.IsNullOrEmpty(result.ServerAddress)) { - tagList.Add(SemanticConventions.AttributeServerAddress, result.ServerAddress); + activity?.AddTag(SemanticConventions.AttributeServerAddress, result.ServerAddress); + meterTags.Add(SemanticConventions.AttributeServerAddress, result.ServerAddress); } if (result.ServerPort is not null) { - tagList.Add(SemanticConventions.AttributeServerPort, result.ServerPort.Value); + activity?.AddTag(SemanticConventions.AttributeServerPort, result.ServerPort.Value); + meterTags.Add(SemanticConventions.AttributeServerPort, result.ServerPort.Value); } if (!result.Database.IsEmpty) { - tagList.Add(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); + activity?.AddTag(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); + meterTags.Add(SemanticConventions.AttributeDbNamespace, result.Database.ToString()); } if (!result.QueryText.IsEmpty) { - var info = KustoProcessor.Process(shouldSummarize: KustoInstrumentation.Options.RecordQuerySummary, shouldSanitize: KustoInstrumentation.Options.RecordQueryText, result.QueryText.ToString()); + var shouldSummarize = KustoInstrumentation.TraceOptions.RecordQuerySummary || KustoInstrumentation.MeterOptions.RecordQuerySummary; + var shouldSanitize = KustoInstrumentation.TraceOptions.RecordQueryText || KustoInstrumentation.MeterOptions.RecordQueryText; + var info = KustoProcessor.Process(shouldSummarize, shouldSanitize, result.QueryText.ToString()); - if (KustoInstrumentation.Options.RecordQueryText) + if (!string.IsNullOrEmpty(info.Sanitized)) { - tagList.Add(SemanticConventions.AttributeDbQueryText, info.Sanitized); + if (KustoInstrumentation.TraceOptions.RecordQueryText) + { + activity?.AddTag(SemanticConventions.AttributeDbQueryText, info.Sanitized); + } + + if (KustoInstrumentation.MeterOptions.RecordQueryText) + { + meterTags.Add(SemanticConventions.AttributeDbQueryText, info.Sanitized); + } } - // Set query summary and use it as display name per spec if (!string.IsNullOrEmpty(info.Summarized)) { - tagList.Add(SemanticConventions.AttributeDbQuerySummary, info.Summarized); - activity?.DisplayName = info.Summarized!; + if (KustoInstrumentation.TraceOptions.RecordQuerySummary) + { + activity?.AddTag(SemanticConventions.AttributeDbQuerySummary, info.Summarized); + activity?.DisplayName = info.Summarized!; + } + + if (KustoInstrumentation.MeterOptions.RecordQuerySummary) + { + meterTags.Add(SemanticConventions.AttributeDbQuerySummary, info.Summarized); + } } } } - this.contexts[record.Activity.ActivityId] = new ContextData(beginTimestamp, tagList, activity!); + this.contexts[record.Activity.ActivityId] = new ContextData(beginTimestamp, meterTags, activity!); this.CallEnrichment(record); } @@ -191,12 +213,11 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) activity?.SetStatus(ActivityStatusCode.Ok); } - activity?.AddTags(context.Value.Tags); this.CallEnrichment(record); activity?.Stop(); var duration = activity?.Duration.TotalSeconds ?? GetElapsedTime(context.Value.BeginTimestamp); - KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, context.Value.Tags); + KustoActivitySourceHelper.OperationDurationHistogram.Record(duration, context.Value.MeterTags); this.contexts.TryRemove(record.Activity.ActivityId, out _); } @@ -217,10 +238,10 @@ private void HandleActivityComplete(KustoUtils.TraceRecord record) /// private readonly struct ContextData { - public ContextData(long beginTimestamp, TagList tags, Activity activity) + public ContextData(long beginTimestamp, TagList meterTags, Activity activity) { this.BeginTimestamp = beginTimestamp; - this.Tags = tags; + this.MeterTags = meterTags; this.Activity = activity; } @@ -231,9 +252,9 @@ public ContextData(long beginTimestamp, TagList tags, Activity activity) public long BeginTimestamp { get; } /// - /// Gets the collection of tags associated with the operation that should be shared between the span and metrics. + /// Gets the collection of tags associated with the operation that should be applies to metrics. /// - public TagList Tags { get; } + public TagList MeterTags { get; } /// /// Gets the current activity associated with the instance, if any. diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterInstrumentationOptions.cs new file mode 100644 index 0000000000..675024e35f --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoMeterInstrumentationOptions.cs @@ -0,0 +1,22 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Metrics; + +/// +/// Options for Kusto meter instrumentation. +/// +public sealed class KustoMeterInstrumentationOptions +{ + /// + /// Gets or sets a value indicating whether the query text should be recorded as an attribute on the activity. + /// Default is . + /// + public bool RecordQueryText { get; set; } + + /// + /// Gets or sets a value indicating whether a summary of the query should be recorded as an attribute on the activity. + /// Default is . + /// + public bool RecordQuerySummary { get; set; } = true; +} diff --git a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.Kusto/KustoTraceInstrumentationOptions.cs similarity index 87% rename from src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs rename to src/OpenTelemetry.Instrumentation.Kusto/KustoTraceInstrumentationOptions.cs index 8891c10e74..a8b4b7c557 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/KustoInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/KustoTraceInstrumentationOptions.cs @@ -4,12 +4,12 @@ using System.Diagnostics; using KustoUtils = Kusto.Cloud.Platform.Utils; -namespace OpenTelemetry.Instrumentation.Kusto; +namespace OpenTelemetry.Trace; /// -/// Options for Kusto instrumentation. +/// Options for Kusto trace instrumentation. /// -public class KustoInstrumentationOptions +public sealed class KustoTraceInstrumentationOptions { /// /// Gets or sets a value indicating whether the query text should be recorded as an attribute on the activity. diff --git a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs index 0f67b1bff8..05bdb82734 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/MeterProviderBuilderExtensions.cs @@ -1,7 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using OpenTelemetry.Instrumentation.Kusto; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; @@ -18,26 +19,29 @@ public static class MeterProviderBuilderExtensions /// being configured. /// The instance of to chain the calls. public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder) => - builder.AddKustoInstrumentation(options => { }); + builder.AddKustoInstrumentation(configureKustoMeterInstrumentationOptions: null); /// /// Enables Kusto instrumentation. /// /// being configured. - /// Action to configure the . + /// Callback action for configuring . /// The instance of to chain the calls. - public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder, Action configureKustoInstrumentationOptions) + public static MeterProviderBuilder AddKustoInstrumentation(this MeterProviderBuilder builder, Action? configureKustoMeterInstrumentationOptions) { Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(configureKustoInstrumentationOptions); - configureKustoInstrumentationOptions(KustoInstrumentation.Options); + if (configureKustoMeterInstrumentationOptions != null) + { + builder.ConfigureServices(services => services.Configure(configureKustoMeterInstrumentationOptions)); + } // Be sure to eagerly initialize the instrumentation, as we must set environment variables before any clients are created. KustoInstrumentation.Initialize(); builder.AddInstrumentation(sp => { + KustoInstrumentation.MeterOptions = sp.GetRequiredService>().CurrentValue; return KustoInstrumentation.HandleManager.AddMetricHandle(); }); diff --git a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj index 0e6035fcd8..8fb946605d 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj +++ b/src/OpenTelemetry.Instrumentation.Kusto/OpenTelemetry.Instrumentation.Kusto.csproj @@ -24,6 +24,8 @@ + + diff --git a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs index 9df41bb9ee..0dd2b57aa0 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Kusto/TracerProviderBuilderExtensions.cs @@ -1,7 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using OpenTelemetry.Instrumentation.Kusto; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.Kusto.Implementation; using OpenTelemetry.Internal; @@ -18,28 +19,31 @@ public static class TracerProviderBuilderExtensions /// being configured. /// The instance of to chain the calls. public static TracerProviderBuilder AddKustoInstrumentation(this TracerProviderBuilder builder) => - AddKustoInstrumentation(builder, options => { }); + AddKustoInstrumentation(builder, configureKustoTraceInstrumentationOptions: null); /// /// Enables Kusto instrumentation. /// /// being configured. - /// Callback action for configuring . + /// Callback action for configuring . /// The instance of to chain the calls. public static TracerProviderBuilder AddKustoInstrumentation( this TracerProviderBuilder builder, - Action configureKustoInstrumentationOptions) + Action? configureKustoTraceInstrumentationOptions) { Guard.ThrowIfNull(builder); - Guard.ThrowIfNull(configureKustoInstrumentationOptions); - configureKustoInstrumentationOptions(KustoInstrumentation.Options); + if (configureKustoTraceInstrumentationOptions != null) + { + builder.ConfigureServices(services => services.Configure(configureKustoTraceInstrumentationOptions)); + } // Be sure to eagerly initialize the instrumentation, as we must set environment variables before any clients are created. KustoInstrumentation.Initialize(); builder.AddInstrumentation(sp => { + KustoInstrumentation.TraceOptions = sp.GetRequiredService>().CurrentValue; return KustoInstrumentation.HandleManager.AddTracingHandle(); }); diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DependencyInjectionConfigTests.cs new file mode 100644 index 0000000000..938543f91a --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/DependencyInjectionConfigTests.cs @@ -0,0 +1,169 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Instrumentation.Kusto.Implementation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.Kusto.Tests; + +public class DependencyInjectionConfigTests : IDisposable +{ + public DependencyInjectionConfigTests() + { + KustoInstrumentation.TraceOptions = new KustoTraceInstrumentationOptions(); + KustoInstrumentation.MeterOptions = new KustoMeterInstrumentationOptions(); + } + + public void Dispose() + { + KustoInstrumentation.TraceOptions = new KustoTraceInstrumentationOptions(); + KustoInstrumentation.MeterOptions = new KustoMeterInstrumentationOptions(); + } + + [Fact] + public void TestTracingOptionsDiConfig() + { + var enrichCalled = false; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + options.Enrich = (activity, record) => { enrichCalled = true; }; + }); + }) + .AddKustoInstrumentation(configureKustoTraceInstrumentationOptions: null) + .Build(); + + // Assert that the options were picked up from DI and set on the static property + Assert.NotNull(KustoInstrumentation.TraceOptions); + Assert.NotNull(KustoInstrumentation.TraceOptions.Enrich); + + // Verify the Enrich callback works + KustoInstrumentation.TraceOptions.Enrich(null!, default!); + Assert.True(enrichCalled); + } + + [Fact] + public void TestMeterOptionsDiConfig() + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + options.RecordQueryText = true; + options.RecordQuerySummary = false; + }); + }) + .AddKustoInstrumentation(configureKustoMeterInstrumentationOptions: null) + .Build(); + + // Assert that the options were picked up from DI and set on the static property + Assert.NotNull(KustoInstrumentation.MeterOptions); + Assert.True(KustoInstrumentation.MeterOptions.RecordQueryText); + Assert.False(KustoInstrumentation.MeterOptions.RecordQuerySummary); + } + + [Fact] + public void TestTraceAndMeterOptionsDiConfigTogether() + { + var enrichCalled = false; + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + options.Enrich = (activity, record) => { enrichCalled = true; }; + }); + }) + .AddKustoInstrumentation(configureKustoTraceInstrumentationOptions: null) + .Build(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + options.RecordQueryText = true; + options.RecordQuerySummary = false; + }); + }) + .AddKustoInstrumentation(configureKustoMeterInstrumentationOptions: null) + .Build(); + + // Assert that both options were picked up from DI and set on the static properties + Assert.NotNull(KustoInstrumentation.TraceOptions); + Assert.NotNull(KustoInstrumentation.TraceOptions.Enrich); + + Assert.NotNull(KustoInstrumentation.MeterOptions); + Assert.True(KustoInstrumentation.MeterOptions.RecordQueryText); + Assert.False(KustoInstrumentation.MeterOptions.RecordQuerySummary); + + // Verify the Enrich callback works + KustoInstrumentation.TraceOptions.Enrich(null!, default!); + Assert.True(enrichCalled); + } + + [Fact] + public void TestTraceOptionsWithCallbackOverridesDi() + { + var enrichFromDiCalled = false; + var enrichFromCallbackCalled = false; + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + // This should be overridden by the callback + options.Enrich = (activity, record) => { enrichFromDiCalled = true; }; + }); + }) + .AddKustoInstrumentation(configureKustoTraceInstrumentationOptions: options => + { + // Callback should override DI configuration + options.Enrich = (activity, record) => { enrichFromCallbackCalled = true; }; + }) + .Build(); + + // Assert that the callback options were used, not the DI options + Assert.NotNull(KustoInstrumentation.TraceOptions); + Assert.NotNull(KustoInstrumentation.TraceOptions.Enrich); + + // Verify the callback version is used + KustoInstrumentation.TraceOptions.Enrich(null!, default!); + Assert.False(enrichFromDiCalled); + Assert.True(enrichFromCallbackCalled); + } + + [Fact] + public void TestMeterOptionsWithCallbackOverridesDi() + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .ConfigureServices(services => + { + services.Configure(options => + { + // This should be overridden by the callback + options.RecordQueryText = false; + options.RecordQuerySummary = true; + }); + }) + .AddKustoInstrumentation(configureKustoMeterInstrumentationOptions: options => + { + // Callback should override DI configuration + options.RecordQueryText = true; + options.RecordQuerySummary = false; + }) + .Build(); + + // Assert that the callback options were used, not the DI options + Assert.NotNull(KustoInstrumentation.MeterOptions); + Assert.True(KustoInstrumentation.MeterOptions.RecordQueryText); + Assert.False(KustoInstrumentation.MeterOptions.RecordQuerySummary); + } +} diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs index 9cf92c55a9..b10105f336 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoIntegrationTests.cs @@ -43,7 +43,11 @@ public async Task SuccessfulQueryTest(string query, bool processQuery) using var meterProvider = Sdk.CreateMeterProviderBuilder() .AddInMemoryExporter(metrics) - .AddKustoInstrumentation() + .AddKustoInstrumentation(options => + { + options.RecordQueryText = processQuery; + options.RecordQuerySummary = processQuery; + }) .Build(); var kcsb = this.fixture.ConnectionStringBuilder; @@ -81,7 +85,11 @@ public async Task TraceOnlyTest(string query) using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddInMemoryExporter(activities) - .AddKustoInstrumentation() + .AddKustoInstrumentation(options => + { + options.RecordQueryText = true; + options.RecordQuerySummary = false; + }) .Build(); var kcsb = this.fixture.ConnectionStringBuilder; @@ -164,7 +172,11 @@ public async Task FailedQueryTest(string query, bool processQuery) using var meterProvider = Sdk.CreateMeterProviderBuilder() .AddInMemoryExporter(metrics) - .AddKustoInstrumentation() + .AddKustoInstrumentation(options => + { + options.RecordQueryText = processQuery; + options.RecordQuerySummary = processQuery; + }) .Build(); var kcsb = this.fixture.ConnectionStringBuilder; diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs index 6e30438a76..93489d7331 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/KustoTraceProviderBuilderTests.cs @@ -26,30 +26,21 @@ public void AddKustoInstrumentation_WithNullBuilder_ThrowsArgumentNullException( Assert.Throws(() => builder.AddKustoInstrumentation()); } - [Fact] - public void AddKustoInstrumentation_WithNullOptions_ThrowsArgumentNullException() - { - var builder = Sdk.CreateTracerProviderBuilder(); - Action options = null!; - - Assert.Throws(() => builder.AddKustoInstrumentation(options)); - } - [Fact] public void AddKustoInstrumentation_WithOptions_DoesNotThrow() { var builder = Sdk.CreateTracerProviderBuilder(); - var actual = builder.AddKustoInstrumentation(options => options.RecordQueryText = true); + var actual = builder.AddKustoInstrumentation(options => options.Enrich = (activity, record) => { }); Assert.Same(builder, actual); } [Fact] - public void KustoInstrumentationOptions_DefaultRecordQueryTextIsFalse() + public void KustoTraceInstrumentationOptions_DefaultEnrichIsNull() { - var options = new KustoInstrumentationOptions(); + var options = new KustoTraceInstrumentationOptions(); - Assert.False(options.RecordQueryText); + Assert.Null(options.Enrich); } } diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt index 02a39c89b0..1a21a7102a 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=False.verified.txt @@ -9,9 +9,6 @@ { azure.kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, - { - error.type: Kusto.Data.Exceptions.SemanticException - }, { db.system.name: azure.kusto }, @@ -26,6 +23,9 @@ }, { db.namespace: NetDefaultDB + }, + { + error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt index 518209b047..fab7c9dfda 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/Snapshots/KustoIntegrationTests.FailedQueryTest_query=InvalidTable - take 10_processQuery=True.verified.txt @@ -9,9 +9,6 @@ { azure.kusto.client_request_id: SW52YWxpZFRhYmxlIHwgdGFrZSAxMA== }, - { - error.type: Kusto.Data.Exceptions.SemanticException - }, { db.system.name: azure.kusto }, @@ -32,6 +29,9 @@ }, { db.query.summary: InvalidTable | take + }, + { + error.type: Kusto.Data.Exceptions.SemanticException } ], OperationName: KD.RestClient.ExecuteQuery, From 77819be252f3303e1a685e5096c619d220ef006d Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 8 Dec 2025 11:27:57 -0800 Subject: [PATCH 45/46] Update docs --- .../CHANGELOG.md | 2 +- .../README.md | 21 ++++++++------- .../README.md | 27 ++++++++++--------- ...lemetry.Instrumentation.Kusto.Tests.csproj | 2 +- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md index 8e73c393cd..6d34ec650a 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/CHANGELOG.md @@ -2,4 +2,4 @@ ## Unreleased -* Initial implementation of Kusto instrumentation. +* Initial implementation. diff --git a/src/OpenTelemetry.Instrumentation.Kusto/README.md b/src/OpenTelemetry.Instrumentation.Kusto/README.md index 7b47a14bd1..f34516a357 100644 --- a/src/OpenTelemetry.Instrumentation.Kusto/README.md +++ b/src/OpenTelemetry.Instrumentation.Kusto/README.md @@ -1,7 +1,7 @@ # Kusto Instrumentation for OpenTelemetry -| Status | | -| ----------- | --------- | +| Status | | +| ----------- | ------------------------------ | | Stability | [Alpha](../../README.md#alpha) | [![NuGet version badge](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.Kusto)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.Kusto) @@ -81,12 +81,12 @@ The instrumentation is implemented based on [metrics semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.36.0/docs/database/database-metrics.md). Currently, the instrumentation supports the following metric: -| Name | Instrument Type | Unit | Description | Attributes | -|--------------------------------|-----------------|------|-----------------------------------------|------------| -| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations. | `db.system`, `db.operation.name`, `db.namespace`, `db.query.summary`¹, `server.address`, `server.port`, `error.type`² | +| Name | Instrument Type | Unit | Description | Attributes | +|--------------------------------|-----------------|------|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| `db.client.operation.duration` | Histogram | `s` | Duration of database client operations. | `db.system`, `db.operation.name`, `db.namespace`, `db.query.summary` (1), `server.address`, `server.port`, `error.type`(2) | -¹ `db.query.summary` is only included when `RecordQuerySummary` is enabled (default: `true`) -² `error.type` is only included when an error occurs +1 `db.query.summary` is only included when `RecordQuerySummary` is enabled +2 `error.type` is only included when an error occurs ## Advanced configuration @@ -142,8 +142,8 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddKustoInstrumentation(opt => opt.Enrich = (activity, record) => { // Add custom tags based on the TraceRecord - activity.SetTag("kusto.activity_id", record.Activity.ActivityId); - activity.SetTag("kusto.activity_type", record.Activity.ActivityType); + activity.SetTag("azure.kusto.activity_id", record.Activity.ActivityId); + activity.SetTag("azure.kusto.activity_type", record.Activity.ActivityType); }) .Build(); ``` @@ -205,7 +205,8 @@ Users | take 100 ``` -Would result in an activity with the summary and display name set to `"Get active users"`. +Would result in an activity with the summary and display name +set to `"Get active users"`. ## References diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md index 073dad000d..2d03f78f69 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md +++ b/test/OpenTelemetry.Instrumentation.Kusto.Benchmarks/README.md @@ -14,13 +14,14 @@ Then choose the benchmark class that you want to run by entering the required option number from the list of options shown on the Console window. > [!TIP] -> The Profiling benchmarks are designed to run quickly and use the Visual Studio diagnosers to gather performance data. +> The Profiling benchmarks are designed to run quickly and use the Visual +> Studio diagnosers to gather performance data. ## Results ### Full instrumentation -``` +```plain BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7093) Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores @@ -30,19 +31,20 @@ Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores ``` + | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |------------------- |----------:|----------:|----------:|-------:|-------:|----------:| -| SuccessfulQuery | 9.043 μs | 0.1183 μs | 0.1048 μs | 0.9308 | 0.0153 | 11.48 KB | -| FailedQuery | 10.076 μs | 0.2007 μs | 0.3354 μs | 0.9613 | 0.0153 | 11.91 KB | -| TraceListenerOnly | 9.411 μs | 0.1788 μs | 0.2325 μs | 0.9308 | 0.0153 | 11.52 KB | -| MetricListenerOnly | 9.352 μs | 0.1746 μs | 0.2613 μs | 0.9308 | 0.0153 | 11.52 KB | +| SuccessfulQuery | 9.043 us | 0.1183 us | 0.1048 us | 0.9308 | 0.0153 | 11.48 KB | +| FailedQuery | 10.076 us | 0.2007 us | 0.3354 us | 0.9613 | 0.0153 | 11.91 KB | +| TraceListenerOnly | 9.411 us | 0.1788 us | 0.2325 us | 0.9308 | 0.0153 | 11.52 KB | +| MetricListenerOnly | 9.352 us | 0.1746 us | 0.2613 us | 0.9308 | 0.0153 | 11.52 KB | ### Summarization and sanitization processing -Summarization and sanitization are the most expensive parts of instrumentation, so there are benchmarks to measure their -specific cost. +Summarization and sanitization are the most expensive parts of instrumentation, so +there are benchmarks to measure their specific cost. -``` +```plain BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7093) Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores @@ -52,9 +54,10 @@ Intel Core Ultra 7 165H 3.80GHz, 1 CPU, 22 logical and 16 physical cores ``` + | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |---------------------------- |----------:|----------:|----------:|-------:|-------:|----------:| -| ProcessSummarizeAndSanitize | 10.141 μs | 0.1926 μs | 0.2436 μs | 1.0834 | 0.0153 | 13.36 KB | -| ProcessSummarizeOnly | 9.549 μs | 0.1772 μs | 0.1571 μs | 1.0071 | 0.0153 | 12.48 KB | -| ProcessSanitizeOnly | 4.154 μs | 0.0827 μs | 0.1832 μs | 0.5798 | 0.0038 | 7.13 KB | +| ProcessSummarizeAndSanitize | 10.141 us | 0.1926 us | 0.2436 us | 1.0834 | 0.0153 | 13.36 KB | +| ProcessSummarizeOnly | 9.549 us | 0.1772 us | 0.1571 us | 1.0071 | 0.0153 | 12.48 KB | +| ProcessSanitizeOnly | 4.154 us | 0.0827 us | 0.1832 us | 0.5798 | 0.0038 | 7.13 KB | | ProcessNeither | 0.0566 ns | 0.0259 ns | 0.0610 ns | - | - | - | diff --git a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj index d8d99b4798..2a189280ce 100644 --- a/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Kusto.Tests/OpenTelemetry.Instrumentation.Kusto.Tests.csproj @@ -34,7 +34,7 @@ + --> From 02565778c3f27ece01a610284072c9253f800410 Mon Sep 17 00:00:00 2001 From: Matt Kotsenas Date: Mon, 8 Dec 2025 13:40:57 -0800 Subject: [PATCH 46/46] Update build files to add new project --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 1 + .github/ISSUE_TEMPLATE/release_request.yml | 1 + .github/codecov.yml | 5 +++++ .github/workflows/ci.yml | 12 ++++++++++++ .github/workflows/prepare-release.yml | 1 + 6 files changed, 21 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6b0ab4808f..7bfe538c29 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -41,6 +41,7 @@ body: - OpenTelemetry.Instrumentation.GrpcNetClient - OpenTelemetry.Instrumentation.Hangfire - OpenTelemetry.Instrumentation.Http + - OpenTelemetry.Instrumentation.Kusto - OpenTelemetry.Instrumentation.MassTransit - OpenTelemetry.Instrumentation.MySqlData - OpenTelemetry.Instrumentation.Owin diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index cbd7ef881d..15d10ba698 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -41,6 +41,7 @@ body: - OpenTelemetry.Instrumentation.GrpcNetClient - OpenTelemetry.Instrumentation.Hangfire - OpenTelemetry.Instrumentation.Http + - OpenTelemetry.Instrumentation.Kusto - OpenTelemetry.Instrumentation.MassTransit - OpenTelemetry.Instrumentation.MySqlData - OpenTelemetry.Instrumentation.Owin diff --git a/.github/ISSUE_TEMPLATE/release_request.yml b/.github/ISSUE_TEMPLATE/release_request.yml index 93ec597d8c..8b85b74924 100644 --- a/.github/ISSUE_TEMPLATE/release_request.yml +++ b/.github/ISSUE_TEMPLATE/release_request.yml @@ -38,6 +38,7 @@ body: - OpenTelemetry.Instrumentation.GrpcNetClient - OpenTelemetry.Instrumentation.Hangfire - OpenTelemetry.Instrumentation.Http + - OpenTelemetry.Instrumentation.Kusto - OpenTelemetry.Instrumentation.MassTransit - OpenTelemetry.Instrumentation.MySqlData - OpenTelemetry.Instrumentation.Owin diff --git a/.github/codecov.yml b/.github/codecov.yml index cb1a79a8e7..5961f03c26 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -135,6 +135,11 @@ flags: paths: - src/OpenTelemetry.Instrumentation.Http + unittests-Instrumentation.Kusto: + carryforward: true + paths: + - src/OpenTelemetry.Instrumentation.Kusto + unittests-Instrumentation.Owin: carryforward: true paths: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f94c12c0c..ce35b24311 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: instrumentation-grpcnetclient: ['*/OpenTelemetry.Instrumentation.GrpcNetClient*/**', '!**/*.md'] instrumentation-hangfire: ['*/OpenTelemetry.Instrumentation.Hangfire*/**', '!**/*.md'] instrumentation-http: ['*/OpenTelemetry.Instrumentation.Http*/**', '!**/*.md'] + instrumentation-kusto: ['*/OpenTelemetry.Instrumentation.Kusto*/**', '!**/*.md'] instrumentation-owin: ['*/OpenTelemetry.Instrumentation.Owin*/**', 'examples/owin/**', '!**/*.md'] instrumentation-process: ['*/OpenTelemetry.Instrumentation.Process*/**', 'examples/process-instrumentation/**', '!**/*.md'] instrumentation-quartz: ['*/OpenTelemetry.Instrumentation.Quartz*/**', '!**/*.md'] @@ -363,6 +364,17 @@ jobs: project-name: Component[OpenTelemetry.Instrumentation.Http] code-cov-name: Instrumentation.Http + build-test-instrumentation-kusto: + needs: detect-changes + if: | + contains(needs.detect-changes.outputs.changes, 'instrumentation-kusto') + || contains(needs.detect-changes.outputs.changes, 'build') + || contains(needs.detect-changes.outputs.changes, 'shared') + uses: ./.github/workflows/Component.BuildTest.yml + with: + project-name: Component[OpenTelemetry.Instrumentation.Kusto] + code-cov-name: Instrumentation.Kusto + build-test-instrumentation-owin: needs: detect-changes if: | diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 95c3b93b1e..4ccca50a26 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -29,6 +29,7 @@ on: - OpenTelemetry.Instrumentation.GrpcNetClient - OpenTelemetry.Instrumentation.Hangfire - OpenTelemetry.Instrumentation.Http + - OpenTelemetry.Instrumentation.Kusto - OpenTelemetry.Instrumentation.MassTransit - OpenTelemetry.Instrumentation.MySqlData - OpenTelemetry.Instrumentation.Owin