From 4fee6df6df3c4408308ca3434487dce78c908d4f Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Mon, 13 Oct 2025 00:31:49 -0700 Subject: [PATCH 1/7] Updating worker instrumentation. --- .../Context/DefaultTraceContext.cs | 9 +- src/DotNetWorker.Core/Context/TraceContext.cs | 9 +- .../Diagnostics/ActivityExtensions.cs | 110 ------------------ .../FunctionActivitySourceFactory.cs | 66 ----------- .../Diagnostics/FunctionInvocationScope.cs | 61 ---------- .../Diagnostics/OpenTelemetrySchemaVersion.cs | 5 +- .../Telemetry/IFunctionTelemetryProvider.cs | 25 ++++ .../Telemetry/TelemetryProviderBase.cs | 55 +++++++++ .../Telemetry/TelemetryProviderV1_17_0.cs | 24 ++++ .../Telemetry/TelemetryProviderV1_37_0.cs | 29 +++++ .../Diagnostics/TraceConstants.cs | 40 +++++-- src/DotNetWorker.Core/FunctionsApplication.cs | 37 ++---- .../Hosting/ServiceCollectionExtensions.cs | 64 +++++++++- .../Hosting/WorkerOptions.cs | 4 +- .../GrpcFunctionInvocation.cs | 4 +- .../GrpcFunctionsHostLogWriter.cs | 4 +- .../ConfigureFunctionsOpenTelemetry.cs | 11 +- .../DotNetWorker.OpenTelemetry.csproj | 6 +- .../OpenTelemetryConstants.cs | 9 +- .../ResourceSemanticConventions.cs | 4 +- .../EndToEndTests.cs | 86 ++++++++++++-- .../ApplicationInsights/EndToEndTests.cs | 8 +- .../Diagnostics/GrpcHostLoggerTests.cs | 10 +- .../FunctionsApplicationTests.cs | 10 +- test/TestUtility/TestFunctionInvocation.cs | 14 ++- 25 files changed, 380 insertions(+), 324 deletions(-) delete mode 100644 src/DotNetWorker.Core/Diagnostics/ActivityExtensions.cs delete mode 100644 src/DotNetWorker.Core/Diagnostics/FunctionActivitySourceFactory.cs delete mode 100644 src/DotNetWorker.Core/Diagnostics/FunctionInvocationScope.cs create mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs create mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs create mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs create mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs diff --git a/src/DotNetWorker.Core/Context/DefaultTraceContext.cs b/src/DotNetWorker.Core/Context/DefaultTraceContext.cs index a3994901f..10e9fdcd9 100644 --- a/src/DotNetWorker.Core/Context/DefaultTraceContext.cs +++ b/src/DotNetWorker.Core/Context/DefaultTraceContext.cs @@ -1,18 +1,23 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Collections.Generic; + namespace Microsoft.Azure.Functions.Worker { internal sealed class DefaultTraceContext : TraceContext { - public DefaultTraceContext(string traceParent, string traceState) + public DefaultTraceContext(string traceParent, string traceState, IReadOnlyDictionary attributes) { TraceParent = traceParent; TraceState = traceState; + Attributes = attributes; } public override string TraceParent { get; } public override string TraceState { get; } + + public override IReadOnlyDictionary Attributes { get; } } } diff --git a/src/DotNetWorker.Core/Context/TraceContext.cs b/src/DotNetWorker.Core/Context/TraceContext.cs index d663db340..c63f95ae2 100644 --- a/src/DotNetWorker.Core/Context/TraceContext.cs +++ b/src/DotNetWorker.Core/Context/TraceContext.cs @@ -1,6 +1,8 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System.Collections.Generic; + namespace Microsoft.Azure.Functions.Worker { /// @@ -17,5 +19,10 @@ public abstract class TraceContext /// Gets the state data. /// public abstract string TraceState { get; } + + /// + /// Gets the attributes associated with the trace. + /// + public abstract IReadOnlyDictionary Attributes { get; } } } diff --git a/src/DotNetWorker.Core/Diagnostics/ActivityExtensions.cs b/src/DotNetWorker.Core/Diagnostics/ActivityExtensions.cs deleted file mode 100644 index 2e53a05c6..000000000 --- a/src/DotNetWorker.Core/Diagnostics/ActivityExtensions.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using System.Diagnostics; -using System.Linq.Expressions; -using System.Reflection; - -namespace Microsoft.Azure.Functions.Worker.Diagnostics -{ - internal static class ActivityExtensions - { - - private static readonly Action _setSpanId; - private static readonly Action _setId; - private static readonly Action _setTraceId; - private static readonly Action _setRootId; - - static ActivityExtensions() - { - BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance; - var activityType = typeof(Activity); - - // Empty setter serves as a safe fallback mechanism to handle cases where the field is not available. - _setSpanId = activityType.GetField("_spanId", flags)?.CreateSetter() ?? ((_, _) => { /* Ignore */ }); - _setId = activityType.GetField("_id", flags)?.CreateSetter() ?? ((_, _) => { /* Ignore */ }); - _setRootId = activityType.GetField("_rootId", flags)?.CreateSetter() ?? ((_, _) => { /* Ignore */ }); - _setTraceId = activityType.GetField("_traceId", flags)?.CreateSetter() ?? ((_, _) => { /* Ignore */ }); - } - - /// - /// Records an exception as an ActivityEvent. - /// - /// The Activity. - /// The exception. - /// If the exception is re-thrown out of the current span, set to true. - /// See https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/exceptions/#recording-an-exception. - /// - public static void RecordException(this Activity activity, Exception ex, bool escaped) - { - if (ex == null) - { - return; - } - - var tagsCollection = new ActivityTagsCollection - { - { TraceConstants.AttributeExceptionType, ex.GetType().FullName }, - { TraceConstants.AttributeExceptionStacktrace, ex.ToString() } - }; - - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - tagsCollection.Add(TraceConstants.AttributeExceptionMessage, ex.Message); - } - - if (escaped) - { - tagsCollection.Add(TraceConstants.AttributeExceptionEscaped, true); - } - - activity?.AddEvent(new ActivityEvent(TraceConstants.AttributeExceptionEventName, default, tagsCollection)); - } - - public static void SetId(this Activity activity, string id) - => _setId(activity, id); - - public static void SetSpanId(this Activity activity, string spanId) - => _setSpanId(activity, spanId); - - public static void SetRootId(this Activity activity, string rootId) - => _setRootId(activity, rootId); - - public static void SetTraceId(this Activity activity, string traceId) - => _setTraceId(activity, traceId); - } - - internal static class FieldInfoExtensionMethods - { - /// - /// Create a re-usable setter for a . - /// When cached and reused, This is quicker than using . - /// - /// The target type of the object. - /// The value type of the field. - /// The field info. - /// A re-usable action to set the field. - internal static Action CreateSetter(this FieldInfo fieldInfo) - { - if (fieldInfo == null) - { - throw new ArgumentNullException(nameof(fieldInfo)); - } - - ParameterExpression targetExp = Expression.Parameter(typeof(TTarget), "target"); - Expression source = targetExp; - - if (fieldInfo.DeclaringType is { } t && t != typeof(TTarget)) - { - source = Expression.Convert(targetExp, t); - } - - // Creating the setter to set the value to the field - ParameterExpression valueExp = Expression.Parameter(typeof(TValue), "value"); - MemberExpression fieldExp = Expression.Field(source, fieldInfo); - BinaryExpression assignExp = Expression.Assign(fieldExp, valueExp); - return Expression.Lambda>(assignExp, targetExp, valueExp).Compile(); - } - } -} diff --git a/src/DotNetWorker.Core/Diagnostics/FunctionActivitySourceFactory.cs b/src/DotNetWorker.Core/Diagnostics/FunctionActivitySourceFactory.cs deleted file mode 100644 index 6b472deb7..000000000 --- a/src/DotNetWorker.Core/Diagnostics/FunctionActivitySourceFactory.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using Microsoft.Extensions.Options; - -namespace Microsoft.Azure.Functions.Worker.Diagnostics -{ - internal class FunctionActivitySourceFactory - { - private static readonly ActivitySource _activitySource = new(TraceConstants.FunctionsActivitySource, TraceConstants.FunctionsActivitySourceVersion); - private readonly string _schemaVersionUrl; - private readonly Lazy> _attributeMap; - - public FunctionActivitySourceFactory(IOptions options) - { - _attributeMap = new Lazy>(() => GetMapping(options.Value.OpenTelemetrySchemaVersion)); - _schemaVersionUrl = TraceConstants.OpenTelemetrySchemaMap[options.Value.OpenTelemetrySchemaVersion]; - } - - public Activity? StartInvoke(FunctionContext context) - { - Activity? activity = null; - - if (_activitySource.HasListeners()) - { - ActivityContext.TryParse(context.TraceContext.TraceParent, context.TraceContext.TraceState, out ActivityContext activityContext); - - activity = _activitySource.StartActivity(TraceConstants.FunctionsInvokeActivityName, ActivityKind.Server, activityContext, - tags: GetTags(context)); - } - - return activity; - } - - /// - /// Provides key mappings for different schema versions. For example, in early versions the invocation id may be - /// represented by "faas.execution" and then later change to "faas.invocation". We want to allow for each of these as - /// exporters may be relying on them. - /// - /// - /// The mapped key name. - /// - private static IReadOnlyDictionary GetMapping(OpenTelemetrySchemaVersion schemaVersion) - { - return schemaVersion switch - { - OpenTelemetrySchemaVersion.v1_17_0 => ImmutableDictionary.Empty, - _ => throw new InvalidOperationException("Schema not supported."), - }; - } - - private IEnumerable> GetTags(FunctionContext context) - { - yield return new(TraceConstants.AttributeSchemaUrl, _schemaVersionUrl); - - string GetKeyMapping(string key) => _attributeMap.Value.GetValueOrDefault(key, key); - - // Using as an example of how to map if schemas change. - yield return new(GetKeyMapping(TraceConstants.AttributeFaasExecution), context.InvocationId); - } - } -} diff --git a/src/DotNetWorker.Core/Diagnostics/FunctionInvocationScope.cs b/src/DotNetWorker.Core/Diagnostics/FunctionInvocationScope.cs deleted file mode 100644 index 49e5d7e17..000000000 --- a/src/DotNetWorker.Core/Diagnostics/FunctionInvocationScope.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Microsoft.Azure.Functions.Worker.Diagnostics -{ - internal class FunctionInvocationScope : IReadOnlyList> - { - internal const string FunctionInvocationIdKey = "AzureFunctions_InvocationId"; - internal const string FunctionNameKey = "AzureFunctions_FunctionName"; - - private readonly string _invocationId; - private readonly string _functionName; - - private string? _cachedToString; - - public FunctionInvocationScope(string functionName, string invocationid) - { - _functionName = functionName; - _invocationId = invocationid; - } - - public KeyValuePair this[int index] - { - get - { - return index switch - { - 0 => new KeyValuePair(FunctionInvocationIdKey, _invocationId), - 1 => new KeyValuePair(FunctionNameKey, _functionName), - _ => throw new ArgumentOutOfRangeException(nameof(index)), - }; - } - } - - public int Count => 2; - - public IEnumerator> GetEnumerator() - { - for (var i = 0; i < Count; ++i) - { - yield return this[i]; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public override string ToString() - { - if (_cachedToString == null) - { - _cachedToString = FormattableString.Invariant($"{FunctionNameKey}:{_functionName} {FunctionInvocationIdKey}:{_invocationId}"); - } - - return _cachedToString; - } - } -} diff --git a/src/DotNetWorker.Core/Diagnostics/OpenTelemetrySchemaVersion.cs b/src/DotNetWorker.Core/Diagnostics/OpenTelemetrySchemaVersion.cs index 8b2e17a92..c24727bfd 100644 --- a/src/DotNetWorker.Core/Diagnostics/OpenTelemetrySchemaVersion.cs +++ b/src/DotNetWorker.Core/Diagnostics/OpenTelemetrySchemaVersion.cs @@ -1,10 +1,11 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. namespace Microsoft.Azure.Functions.Worker.Diagnostics { internal enum OpenTelemetrySchemaVersion { - v1_17_0 + V1_17_0, + V1_37_0 } } diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs new file mode 100644 index 000000000..4b856da6d --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +/// +/// Provides methods for telemetry data collection related to function invocations. +/// +/// This interface defines methods to retrieve telemetry attributes and manage the activity lifecycle for +/// function invocations, enabling detailed monitoring and diagnostics. +internal interface IFunctionTelemetryProvider +{ + /// + /// Returns the attributes to be applied to the Activity/Scope for this invocation. + /// + IEnumerable> GetTelemetryAttributes(FunctionContext ctx); + + /// + /// Starts the Activity for this invocation. + /// + Activity? StartActivityForInvocation(FunctionContext ctx); +} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs new file mode 100644 index 000000000..f8ea7b584 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +internal abstract class TelemetryProviderBase : IFunctionTelemetryProvider +{ + private static readonly ActivitySource _source + = new(TraceConstants.FunctionsActivitySource, TraceConstants.FunctionsActivitySourceVersion); + + protected abstract OpenTelemetrySchemaVersion SchemaVersion { get; } + + protected abstract ActivityKind Kind { get; } + + public Activity? StartActivityForInvocation(FunctionContext context) + { + if (!_source.HasListeners()) + { + return null; + } + + ActivityContext.TryParse( + context.TraceContext.TraceParent, + context.TraceContext.TraceState, + out var parent); + + // If there is no parent, we still want to create a new root activity. + return _source.StartActivity( + TraceConstants.FunctionsInvokeActivityName, + Kind, + parent, + tags: GetTelemetryAttributes(context)!); + } + + public IEnumerable> GetTelemetryAttributes(FunctionContext context) + { + // Live-logs session + if (context.TraceContext.Attributes.TryGetValue(TraceConstants.AzFuncLiveLogsSessionIdKey, out var liveId) + && !string.IsNullOrWhiteSpace(liveId)) + { + yield return new(TraceConstants.AzFuncLiveLogsSessionIdKey, liveId); + } + + // Version-specific tags + foreach (var kv in GetVersionSpecificAttributes(context)) + { + yield return kv; + } + } + + protected abstract IEnumerable> GetVersionSpecificAttributes(FunctionContext context); +} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs new file mode 100644 index 000000000..71bbcde00 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +internal sealed class TelemetryProviderV1_17_0 : TelemetryProviderBase +{ + protected override OpenTelemetrySchemaVersion SchemaVersion + => OpenTelemetrySchemaVersion.V1_17_0; + + protected override ActivityKind Kind + => ActivityKind.Server; + + protected override IEnumerable> GetVersionSpecificAttributes(FunctionContext context) + { + yield return new(TraceConstants.AttributeAzSchemaUrl, TraceConstants.OpenTelemetrySchemaMap[SchemaVersion]); + yield return new(TraceConstants.FunctionInvocationIdKey, context.InvocationId); + yield return new(TraceConstants.FunctionNameKey, context.FunctionDefinition.Name); + yield return new(TraceConstants.AttributeFaasExecution, context.InvocationId); + } +} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs new file mode 100644 index 000000000..2d3db654b --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +internal sealed class TelemetryProviderV1_37_0 : TelemetryProviderBase +{ + protected override OpenTelemetrySchemaVersion SchemaVersion + => OpenTelemetrySchemaVersion.V1_37_0; + + protected override ActivityKind Kind + => ActivityKind.Internal; + + protected override IEnumerable> GetVersionSpecificAttributes(FunctionContext context) + { + yield return new(TraceConstants.AttributeSchemaUrl, TraceConstants.OpenTelemetrySchemaMap[SchemaVersion]); + yield return new(TraceConstants.AttributeFaasInvocationId, context.InvocationId); + yield return new(TraceConstants.AttributeFaasFunctionName, context.FunctionDefinition.Name); + + if (context.TraceContext.Attributes.TryGetValue(TraceConstants.HostInstanceIdKey, out var host) + && !string.IsNullOrEmpty(host)) + { + yield return new(TraceConstants.AttributeFaasInstance, host); + } + } +} diff --git a/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs b/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs index c8d7ecafc..d1d3bacf5 100644 --- a/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs +++ b/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; @@ -7,8 +7,15 @@ namespace Microsoft.Azure.Functions.Worker.Diagnostics { internal class TraceConstants { - public const string FunctionsActivitySource = "Microsoft.Azure.Functions.Worker"; - public const string FunctionsActivitySourceVersion = "1.0.0.0"; + public static IReadOnlyDictionary OpenTelemetrySchemaMap = + new Dictionary() + { + [OpenTelemetrySchemaVersion.V1_17_0] = "https://opentelemetry.io/schemas/1.17.0", + [OpenTelemetrySchemaVersion.V1_37_0] = "https://opentelemetry.io/schemas/1.37.0" + }; + + public static readonly string FunctionsActivitySourceVersion = typeof(TraceConstants).Assembly.GetName().Version?.ToString() ?? string.Empty; + public const string FunctionsActivitySource = "Microsoft.Azure.Functions.Worker"; public const string FunctionsInvokeActivityName = "Invoke"; public const string AttributeExceptionEventName = "exception"; @@ -17,15 +24,24 @@ internal class TraceConstants public const string AttributeExceptionStacktrace = "exception.stacktrace"; public const string AttributeExceptionEscaped = "exception.escaped"; - public const string AttributeSchemaUrl = "az.schema_url"; - public static IReadOnlyDictionary OpenTelemetrySchemaMap = - new Dictionary() - { - [OpenTelemetrySchemaVersion.v1_17_0] = "https://opentelemetry.io/schemas/1.17.0" - }; - - // from: https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/faas/ - // https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/faas/ + // v1.17.0 attributes public const string AttributeFaasExecution = "faas.execution"; + public const string AttributeAzSchemaUrl = "az.schema_url"; + + // v1.37.0 attributes + public const string AttributeFaasInvocationId = "faas.invocation_id"; + public const string AttributeFaasFunctionName = "faas.name"; + public const string AttributeFaasInstance = "faas.instance"; + public const string AttributeSchemaUrl = "schema.url"; + + // Internal keys used for mapping + internal const string FunctionInvocationIdKey = "AzureFunctions_InvocationId"; + internal const string FunctionNameKey = "AzureFunctions_FunctionName"; + internal const string HostInstanceIdKey = "HostInstanceId"; + internal const string AzFuncLiveLogsSessionIdKey = "#AzFuncLiveLogsSessionId"; + + // Capability variables + internal const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; + internal const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; } } diff --git a/src/DotNetWorker.Core/FunctionsApplication.cs b/src/DotNetWorker.Core/FunctionsApplication.cs index 11db9d003..b4cbeb347 100644 --- a/src/DotNetWorker.Core/FunctionsApplication.cs +++ b/src/DotNetWorker.Core/FunctionsApplication.cs @@ -1,9 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Functions.Worker.Diagnostics; @@ -22,7 +23,7 @@ internal partial class FunctionsApplication : IFunctionsApplication private readonly IOptions _workerOptions; private readonly ILogger _logger; private readonly IWorkerDiagnostics _diagnostics; - private readonly FunctionActivitySourceFactory _functionActivitySourceFactory; + private readonly IFunctionTelemetryProvider _functionTelemetryProvider; public FunctionsApplication( FunctionExecutionDelegate functionExecutionDelegate, @@ -30,14 +31,14 @@ public FunctionsApplication( IOptions workerOptions, ILogger logger, IWorkerDiagnostics diagnostics, - FunctionActivitySourceFactory functionActivitySourceFactory) + IFunctionTelemetryProvider functionTelemetryProvider) { _functionExecutionDelegate = functionExecutionDelegate ?? throw new ArgumentNullException(nameof(functionExecutionDelegate)); _functionContextFactory = functionContextFactory ?? throw new ArgumentNullException(nameof(functionContextFactory)); _workerOptions = workerOptions ?? throw new ArgumentNullException(nameof(workerOptions)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); - _functionActivitySourceFactory = functionActivitySourceFactory ?? throw new ArgumentNullException(nameof(functionActivitySourceFactory)); + _functionTelemetryProvider = functionTelemetryProvider ?? throw new ArgumentNullException(nameof(functionTelemetryProvider)); } public FunctionContext CreateContext(IInvocationFeatures features, CancellationToken token = default) @@ -67,29 +68,8 @@ public void LoadFunction(FunctionDefinition definition) public async Task InvokeFunctionAsync(FunctionContext context) { - Activity? activity = null; - - if (Activity.Current is null) - { - // This will act as an internal activity that represents remote Host activity. This cannot be tracked as this is not associate to an ActivitySource. - activity = new Activity(nameof(InvokeFunctionAsync)); - activity.Start(); - - if (ActivityContext.TryParse(context.TraceContext.TraceParent, context.TraceContext.TraceState, true, out ActivityContext activityContext)) - { - activity.SetId(context.TraceContext.TraceParent); - activity.SetSpanId(activityContext.SpanId.ToString()); - activity.SetTraceId(activityContext.TraceId.ToString()); - activity.SetRootId(activityContext.TraceId.ToString()); - activity.ActivityTraceFlags = activityContext.TraceFlags; - activity.TraceStateString = activityContext.TraceState; - } - } - - var scope = new FunctionInvocationScope(context.FunctionDefinition.Name, context.InvocationId); - - using var logScope = _logger.BeginScope(scope); - using Activity? invokeActivity = _functionActivitySourceFactory.StartInvoke(context); + using var logScope = _logger.BeginScope(_functionTelemetryProvider.GetTelemetryAttributes(context).ToList()); + using Activity? invokeActivity = _functionTelemetryProvider.StartActivityForInvocation(context); try { @@ -103,9 +83,6 @@ public async Task InvokeFunctionAsync(FunctionContext context) throw; } - - invokeActivity?.Stop(); - activity?.Stop(); } } } diff --git a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs index 8ee1643de..70f536981 100644 --- a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -16,7 +16,6 @@ using Microsoft.Azure.Functions.Worker.OutputBindings; using Microsoft.Azure.Functions.Worker.Pipeline; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -81,8 +80,7 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe services.TryAddSingleton(s => s.GetRequiredService()); services.TryAddSingleton(s => s.GetRequiredService()); services.TryAddSingleton(s => s.GetRequiredService()); - services.TryAddSingleton(); - + if (configure != null) { services.Configure(configure); @@ -111,6 +109,8 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe RunExtensionStartupCode(builder); } + services.AddFunctionTelemetry(); + return builder; } @@ -123,6 +123,46 @@ internal static IServiceCollection AddDefaultInputConvertersToWorkerOptions(this return services; } + /// + /// Adds function telemetry services to the specified . + /// + /// This method registers a singleton + /// implementation based on the OpenTelemetry schema version specified in the worker options. If the + /// "WorkerOpenTelemetryEnabled" capability is set to , the schema version is determined + /// from the "WorkerOpenTelemetrySchemaVersion" capability. If the schema version is unsupported, an is thrown. + /// The to which the telemetry services are added. + /// The modified with telemetry services registered. + /// Thrown if the specified OpenTelemetry schema version is unsupported. + internal static IServiceCollection AddFunctionTelemetry(this IServiceCollection services) + { + services.TryAddSingleton(sp => + { + WorkerOptions options = sp.GetRequiredService>().Value; + + bool otelFlag = options.Capabilities.TryGetValue(TraceConstants.WorkerOTelEnabled, out var flag) + && bool.TryParse(flag, out bool value) && value; + + // Always start at the legacy default + OpenTelemetrySchemaVersion version = OpenTelemetrySchemaVersion.V1_17_0; + + // If OTEL is on and a schema-capability is present, parse it (or throw) + if (otelFlag && options.Capabilities.TryGetValue(TraceConstants.WorkerOTelSchemaVersion, out var sv)) + { + version = ParseSchemaVersion(sv); + } + + return version switch + { + OpenTelemetrySchemaVersion.V1_37_0 => new TelemetryProviderV1_37_0(), + OpenTelemetrySchemaVersion.V1_17_0 => new TelemetryProviderV1_17_0(), + _ => throw new InvalidOperationException($"Unsupported OpenTelemetry schema version: {version}") + }; + }); + + return services; + } + /// /// Run extension startup execution code. /// Our source generator creates a class(WorkerExtensionStartupCodeExecutor) @@ -148,6 +188,22 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b startupCodeExecutorInstance!.Configure(builder); } + + /// + /// Maps only known version strings to the enum. + /// If the string is anything else (and was explicitly set), we throw. + /// + private static OpenTelemetrySchemaVersion ParseSchemaVersion(string version) + { + return version switch + { + "1.17.0" => OpenTelemetrySchemaVersion.V1_17_0, + "1.37.0" => OpenTelemetrySchemaVersion.V1_37_0, + _ => throw new ArgumentException( + $"Invalid OpenTelemetry schema version '{version}'. ", nameof(version)) + }; + } + private sealed class WorkerOptionsSetup(IOptions serializerOptions) : IPostConfigureOptions { public void PostConfigure(string? name, WorkerOptions options) diff --git a/src/DotNetWorker.Core/Hosting/WorkerOptions.cs b/src/DotNetWorker.Core/Hosting/WorkerOptions.cs index c71b6dc1e..a360e4183 100644 --- a/src/DotNetWorker.Core/Hosting/WorkerOptions.cs +++ b/src/DotNetWorker.Core/Hosting/WorkerOptions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -66,7 +66,7 @@ public bool IncludeEmptyEntriesInMessagePayload /// Gets or sets a value that determines the schema to use when generating Activities. Currently internal as there is only /// one schema, but stubbing this out for future use. /// - internal OpenTelemetrySchemaVersion OpenTelemetrySchemaVersion { get; set; } = OpenTelemetrySchemaVersion.v1_17_0; + internal OpenTelemetrySchemaVersion OpenTelemetrySchemaVersion { get; set; } = OpenTelemetrySchemaVersion.V1_17_0; private bool GetBoolCapability(string name) { diff --git a/src/DotNetWorker.Grpc/GrpcFunctionInvocation.cs b/src/DotNetWorker.Grpc/GrpcFunctionInvocation.cs index 0cf79374d..3e14df92d 100644 --- a/src/DotNetWorker.Grpc/GrpcFunctionInvocation.cs +++ b/src/DotNetWorker.Grpc/GrpcFunctionInvocation.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using Microsoft.Azure.Functions.Worker.Grpc.Messages; @@ -13,7 +13,7 @@ internal sealed class GrpcFunctionInvocation : FunctionInvocation, IExecutionRet public GrpcFunctionInvocation(InvocationRequest invocationRequest) { _invocationRequest = invocationRequest; - TraceContext = new DefaultTraceContext(_invocationRequest.TraceContext.TraceParent, _invocationRequest.TraceContext.TraceState); + TraceContext = new DefaultTraceContext(_invocationRequest.TraceContext.TraceParent, _invocationRequest.TraceContext.TraceState, _invocationRequest.TraceContext.Attributes); } public override string Id => _invocationRequest.InvocationId; diff --git a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs index f04cd3a5d..aecd71d22 100644 --- a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs +++ b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -88,7 +88,7 @@ private RpcLog AppendInvocationIdToLog(RpcLog rpcLog, IExternalScopeProvider sco { foreach (var pair in properties) { - if (pair.Key == FunctionInvocationScope.FunctionInvocationIdKey) + if (pair.Key == TraceConstants.FunctionInvocationIdKey) { log.InvocationId = pair.Value?.ToString(); break; diff --git a/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs b/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs index a31ab6e8a..b78fd3a86 100644 --- a/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs +++ b/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -18,13 +18,20 @@ public static IOpenTelemetryBuilder UseFunctionsWorkerDefaults(this IOpenTelemet builder.Services // Tells the host to no longer emit telemetry on behalf of the worker. - .Configure(workerOptions => workerOptions.Capabilities["WorkerOpenTelemetryEnabled"] = bool.TrueString); + .Configure(workerOptions => workerOptions.Capabilities[OpenTelemetryConstants.WorkerOTelEnabled] = bool.TrueString) + .Configure(workerOptions => workerOptions.Capabilities[OpenTelemetryConstants.WorkerOTelSchemaVersion] = OpenTelemetryConstants.WorkerSchemaVersion); builder.ConfigureResource((resourceBuilder) => { resourceBuilder.AddDetector(new FunctionsResourceDetector()); }); + // Add the ActivitySource so traces from the Functions Worker are captured + builder.WithTracing(tracerProviderBuilder => + { + tracerProviderBuilder.AddSource(OpenTelemetryConstants.WorkerActivitySourceName); + }); + return builder; } } diff --git a/src/DotNetWorker.OpenTelemetry/DotNetWorker.OpenTelemetry.csproj b/src/DotNetWorker.OpenTelemetry/DotNetWorker.OpenTelemetry.csproj index e9d0947e4..d38c753b6 100644 --- a/src/DotNetWorker.OpenTelemetry/DotNetWorker.OpenTelemetry.csproj +++ b/src/DotNetWorker.OpenTelemetry/DotNetWorker.OpenTelemetry.csproj @@ -16,9 +16,9 @@ - - - + + + diff --git a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs index 5cdca79b3..05f7a584c 100644 --- a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs +++ b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. namespace Microsoft.Azure.Functions.Worker.OpenTelemetry @@ -12,5 +12,12 @@ internal class OpenTelemetryConstants internal const string RegionNameEnvVar = "REGION_NAME"; internal const string ResourceGroupEnvVar = "WEBSITE_RESOURCE_GROUP"; internal const string OwnerNameEnvVar = "WEBSITE_OWNER_NAME"; + internal const string WorkerSchemaVersion = "1.37.0"; + internal const string WorkerActivitySourceName = "Microsoft.Azure.Functions.Worker"; + + + // Capability variables + internal const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; + internal const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; } } diff --git a/src/DotNetWorker.OpenTelemetry/ResourceSemanticConventions.cs b/src/DotNetWorker.OpenTelemetry/ResourceSemanticConventions.cs index 1cc8b07b3..28d9017a7 100644 --- a/src/DotNetWorker.OpenTelemetry/ResourceSemanticConventions.cs +++ b/src/DotNetWorker.OpenTelemetry/ResourceSemanticConventions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; @@ -14,7 +14,7 @@ internal static class ResourceSemanticConventions internal const string CloudProvider = "cloud.provider"; internal const string CloudPlatform = "cloud.platform"; internal const string CloudRegion = "cloud.region"; - internal const string CloudResourceId = "cloud.resource.id"; + internal const string CloudResourceId = "cloud.resource_id"; // Process internal const string ProcessId = "process.pid"; diff --git a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs index 1a60ba907..ed60e613a 100644 --- a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs +++ b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -32,7 +32,7 @@ public class EndToEndTests private IInvocationFeaturesFactory _invocationFeaturesFactory; private readonly OtelFunctionDefinition _funcDefinition = new(); - private IHost InitializeHost() + private IHost InitializeHost(string schemaVersion = null) { var host = new HostBuilder() .ConfigureServices(services => @@ -45,6 +45,13 @@ private IHost InitializeHost() services.AddSingleton(_ => new Mock().Object); }) + .ConfigureFunctionsWorkerDefaults((WorkerOptions options) => + { + if (schemaVersion is not null) + { + options.Capabilities["WorkerOpenTelemetrySchemaVersion"] = schemaVersion; + } + }) .Build(); _application = host.Services.GetService(); @@ -72,19 +79,19 @@ private FunctionContext CreateContext(IHost host) [Fact] public async Task ContextPropagation() { + using var testListener = new ActivityTestListener("Microsoft.Azure.Functions.Worker"); using var host = InitializeHost(); - var context = CreateContext(host); + var context = CreateContext(host); await _application.InvokeFunctionAsync(context); var activity = OtelFunctionDefinition.LastActivity; if (ActivityContext.TryParse(context.TraceContext.TraceParent, context.TraceContext.TraceState, true, out ActivityContext activityContext)) { - Assert.Equal(activity.Id, context.TraceContext.TraceParent); - Assert.Equal("InvokeFunctionAsync", activity.OperationName); - Assert.Equal(activity.SpanId, activityContext.SpanId); + Assert.Equal("Invoke", activity.OperationName); Assert.Equal(activity.TraceId, activityContext.TraceId); - Assert.Equal(activity.ActivityTraceFlags, activityContext.TraceFlags); Assert.Equal(activity.TraceStateString, activityContext.TraceState); + Assert.Equal(ActivityKind.Internal, activity.Kind); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AttributeFaasInvocationId && t.Value == context.InvocationId); } else { @@ -92,6 +99,45 @@ public async Task ContextPropagation() } } + [Fact] + public async Task ContextPropagationV17() + { + using var testListener = new ActivityTestListener("Microsoft.Azure.Functions.Worker"); + using var host = InitializeHost("1.17.0"); + var context = CreateContext(host); + await _application.InvokeFunctionAsync(context); + var activity = OtelFunctionDefinition.LastActivity; + + if (ActivityContext.TryParse(context.TraceContext.TraceParent, context.TraceContext.TraceState, true, out ActivityContext activityContext)) + { + Assert.Equal("Invoke", activity.OperationName); + Assert.Equal(activity.TraceId, activityContext.TraceId); + Assert.Equal(activity.TraceStateString, activityContext.TraceState); + Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AttributeFaasExecution && t.Value == context.InvocationId); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AzFuncLiveLogsSessionIdKey && t.Value == context.TraceContext.Attributes[TraceConstants.AzFuncLiveLogsSessionIdKey]); + } + else + { + Assert.Fail("Failed to parse ActivityContext"); + } + } + + [Fact] + public async Task ContextPropagation_InvalidVersion() + { + try + { + using var host = InitializeHost("0.0.0"); + var context = CreateContext(host); + await _application.InvokeFunctionAsync(context); + } + catch (Exception ex) + { + Assert.IsType(ex); + } + } + [Fact] public async Task ContextPropagationWithTriggerInstrumentation() { @@ -129,7 +175,7 @@ public void ResourceDetector() Resource resource = detector.Detect(); Assert.Equal($"/subscriptions/AAAAA-AAAAA-AAAAA-AAA/resourceGroups/rg/providers/Microsoft.Web/sites/appName" - , resource.Attributes.FirstOrDefault(a => a.Key == "cloud.resource.id").Value); + , resource.Attributes.FirstOrDefault(a => a.Key == "cloud.resource_id").Value); Assert.Equal($"EastUS", resource.Attributes.FirstOrDefault(a => a.Key == "cloud.region").Value); } @@ -198,4 +244,28 @@ protected override async Task SendAsync(HttpRequestMessage return await Task.FromResult(_response); } } + + internal sealed class ActivityTestListener : IDisposable + { + public List Activities { get; } = new List(); + private readonly ActivityListener _listener; + + public ActivityTestListener(string sourceName) + { + _listener = new ActivityListener + { + ShouldListenTo = s => s.Name == sourceName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStarted = activity => Activities.Add(activity), + ActivityStopped = _ => { } + }; + + ActivitySource.AddActivityListener(_listener); + } + + public void Dispose() + { + _listener.Dispose(); + } + } } diff --git a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs index f4914517f..b1b4a9900 100644 --- a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs +++ b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -209,7 +209,7 @@ private static void ValidateDependencyTelemetry(DependencyTelemetry dependency, Assert.Equal(activity.RootId, dependency.Context.Operation.Id); Assert.Equal(context.InvocationId, dependency.Properties[TraceConstants.AttributeFaasExecution]); - Assert.Contains(TraceConstants.AttributeSchemaUrl, dependency.Properties.Keys); + Assert.Contains(TraceConstants.AttributeAzSchemaUrl, dependency.Properties.Keys); ValidateCommonTelemetry(dependency); } @@ -220,8 +220,8 @@ private static void ValidateTraceTelemetry(TraceTelemetry trace, FunctionContext Assert.Equal(SeverityLevel.Warning, trace.SeverityLevel); // Check that scopes show up by default - Assert.Equal("TestName", trace.Properties[FunctionInvocationScope.FunctionNameKey]); - Assert.Equal(context.InvocationId, trace.Properties[FunctionInvocationScope.FunctionInvocationIdKey]); + Assert.Equal("TestName", trace.Properties[TraceConstants.FunctionNameKey]); + Assert.Equal(context.InvocationId, trace.Properties[TraceConstants.FunctionInvocationIdKey]); Assert.Equal(activity.RootId, trace.Context.Operation.Id); diff --git a/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs b/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs index 73d457c08..8e7afbb04 100644 --- a/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs +++ b/test/DotNetWorkerTests/Diagnostics/GrpcHostLoggerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -95,7 +95,13 @@ public async Task SystemLog_WithException_AndScope() { Exception thrownException = null; - using (_scopeProvider.Push(new FunctionInvocationScope("MyFunction", "MyInvocationId"))) + var scopeValues = new Dictionary + { + ["AzureFunctions_InvocationId"] = "MyInvocationId", + ["AzureFunctions_FunctionName"] = "MyFunction" + }; + + using (_scopeProvider.Push(scopeValues)) { try { diff --git a/test/DotNetWorkerTests/FunctionsApplicationTests.cs b/test/DotNetWorkerTests/FunctionsApplicationTests.cs index 05c84c669..e6c165bfc 100644 --- a/test/DotNetWorkerTests/FunctionsApplicationTests.cs +++ b/test/DotNetWorkerTests/FunctionsApplicationTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -72,7 +72,7 @@ static Task Invoke(FunctionContext context) var app = CreateApplication(Invoke, logger); await app.InvokeFunctionAsync(context); - } + } [Fact] public async Task InvokeAsync_OnError_RecordsActivity() @@ -104,7 +104,7 @@ static Task InvokeWithError(FunctionContext context) private static void AssertActivity(Activity activity, FunctionContext context) { Assert.Equal("Invoke", activity.DisplayName); - Assert.Equal(2, activity.Tags.Count()); + Assert.Equal(5, activity.Tags.Count()); Assert.Equal("https://opentelemetry.io/schemas/1.17.0", activity.Tags.Single(k => k.Key == "az.schema_url").Value); Assert.Equal(context.InvocationId, activity.Tags.Single(k => k.Key == "faas.execution").Value); } @@ -114,9 +114,9 @@ private static FunctionsApplication CreateApplication(FunctionExecutionDelegate var options = new OptionsWrapper(new WorkerOptions()); var contextFactory = new Mock(); var diagnostics = new Mock(); - var activityFactory = new FunctionActivitySourceFactory(new OptionsWrapper(new WorkerOptions())); + var telemetryProvider = new TelemetryProviderV1_17_0(); - return new FunctionsApplication(invoke, contextFactory.Object, options, logger, diagnostics.Object, activityFactory); + return new FunctionsApplication(invoke, contextFactory.Object, options, logger, diagnostics.Object, telemetryProvider); } private static ActivityListener CreateListener(Action onStopped) diff --git a/test/TestUtility/TestFunctionInvocation.cs b/test/TestUtility/TestFunctionInvocation.cs index 2004d5e61..0a8f3130b 100644 --- a/test/TestUtility/TestFunctionInvocation.cs +++ b/test/TestUtility/TestFunctionInvocation.cs @@ -1,8 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics; +using Microsoft.Azure.Functions.Worker.Diagnostics; namespace Microsoft.Azure.Functions.Worker.Tests { @@ -21,8 +23,14 @@ public TestFunctionInvocation(string id = null, string functionId = null) } // create/dispose activity to pull off its ID. - using Activity activity = new Activity(string.Empty).Start(); - TraceContext = new DefaultTraceContext(activity.Id, Guid.NewGuid().ToString()); + using Activity activity = new Activity("Test").Start(); + Dictionary attributes = new Dictionary + { + { TraceConstants.FunctionInvocationIdKey, Guid.NewGuid().ToString() }, + { TraceConstants.AzFuncLiveLogsSessionIdKey, Guid.NewGuid().ToString() }, + }; + + TraceContext = new DefaultTraceContext(activity.Id, Guid.NewGuid().ToString(), attributes); } public override string Id { get; } = Guid.NewGuid().ToString(); From e6922ed11229a821ff2c5116abd45a97e9eaae87 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Mon, 13 Oct 2025 00:48:44 -0700 Subject: [PATCH 2/7] Remove empty lines. --- src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs | 1 - src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs index 70f536981..6def63b78 100644 --- a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs @@ -188,7 +188,6 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b startupCodeExecutorInstance!.Configure(builder); } - /// /// Maps only known version strings to the enum. /// If the string is anything else (and was explicitly set), we throw. diff --git a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs index 05f7a584c..01d6ae3f0 100644 --- a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs +++ b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs @@ -15,7 +15,6 @@ internal class OpenTelemetryConstants internal const string WorkerSchemaVersion = "1.37.0"; internal const string WorkerActivitySourceName = "Microsoft.Azure.Functions.Worker"; - // Capability variables internal const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; internal const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; From e8f5a11648714e9eb78f8b134d342b2fc1b2dcb1 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Mon, 13 Oct 2025 00:52:41 -0700 Subject: [PATCH 3/7] Adding a test for empty version. --- .../EndToEndTests.cs | 19 +++++++++++++++++-- .../FunctionsApplicationTests.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs index ed60e613a..2ce9d926a 100644 --- a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs +++ b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs @@ -81,7 +81,7 @@ public async Task ContextPropagation() { using var testListener = new ActivityTestListener("Microsoft.Azure.Functions.Worker"); using var host = InitializeHost(); - var context = CreateContext(host); + var context = CreateContext(host); await _application.InvokeFunctionAsync(context); var activity = OtelFunctionDefinition.LastActivity; @@ -129,7 +129,22 @@ public async Task ContextPropagation_InvalidVersion() try { using var host = InitializeHost("0.0.0"); - var context = CreateContext(host); + var context = CreateContext(host); + await _application.InvokeFunctionAsync(context); + } + catch (Exception ex) + { + Assert.IsType(ex); + } + } + + [Fact] + public async Task ContextPropagation_EmptyVersion() + { + try + { + using var host = InitializeHost(string.Empty); + var context = CreateContext(host); await _application.InvokeFunctionAsync(context); } catch (Exception ex) diff --git a/test/DotNetWorkerTests/FunctionsApplicationTests.cs b/test/DotNetWorkerTests/FunctionsApplicationTests.cs index e6c165bfc..a97720876 100644 --- a/test/DotNetWorkerTests/FunctionsApplicationTests.cs +++ b/test/DotNetWorkerTests/FunctionsApplicationTests.cs @@ -72,7 +72,7 @@ static Task Invoke(FunctionContext context) var app = CreateApplication(Invoke, logger); await app.InvokeFunctionAsync(context); - } + } [Fact] public async Task InvokeAsync_OnError_RecordsActivity() From 422b21e657167848f388ea02fecf22d5415017f9 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan Date: Tue, 14 Oct 2025 15:39:25 -0700 Subject: [PATCH 4/7] Addressing review comments. --- src/DotNetWorker.Core/Context/TraceContext.cs | 2 +- .../Telemetry/TelemetryProvider.cs | 86 +++++++++++++++++++ .../Telemetry/TelemetryProviderBase.cs | 55 ------------ .../Telemetry/TelemetryProviderV1_17_0.cs | 20 +++-- .../Telemetry/TelemetryProviderV1_37_0.cs | 23 +++-- .../Diagnostics/TraceConstants.cs | 86 +++++++++++-------- .../Hosting/ServiceCollectionExtensions.cs | 40 +-------- .../GrpcFunctionsHostLogWriter.cs | 2 +- .../ConfigureFunctionsOpenTelemetry.cs | 2 +- .../OpenTelemetryConstants.cs | 2 +- .../EndToEndTests.cs | 8 +- .../ApplicationInsights/EndToEndTests.cs | 10 +-- .../FunctionsApplicationTests.cs | 2 +- test/TestUtility/TestFunctionInvocation.cs | 4 +- 14 files changed, 183 insertions(+), 159 deletions(-) create mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs delete mode 100644 src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs diff --git a/src/DotNetWorker.Core/Context/TraceContext.cs b/src/DotNetWorker.Core/Context/TraceContext.cs index c63f95ae2..ee1017217 100644 --- a/src/DotNetWorker.Core/Context/TraceContext.cs +++ b/src/DotNetWorker.Core/Context/TraceContext.cs @@ -23,6 +23,6 @@ public abstract class TraceContext /// /// Gets the attributes associated with the trace. /// - public abstract IReadOnlyDictionary Attributes { get; } + public virtual IReadOnlyDictionary Attributes => new Dictionary(); } } diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs new file mode 100644 index 000000000..0ed8af1f6 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +internal abstract class TelemetryProvider : IFunctionTelemetryProvider +{ + private static readonly ActivitySource _source + = new(TraceConstants.ActivityAttributes.Name, TraceConstants.ActivityAttributes.Version); + + protected abstract OpenTelemetrySchemaVersion SchemaVersion { get; } + + protected abstract ActivityKind Kind { get; } + + + public static TelemetryProvider Create(string? schema = null) + { + if (string.IsNullOrWhiteSpace(schema)) + { + return Create(OpenTelemetrySchemaVersion.V1_17_0); + } + + var version = ParseSchemaVersion(schema!); + return Create(version); + } + + public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) + { + return version switch + { + OpenTelemetrySchemaVersion.V1_17_0 => new TelemetryProviderV1_17_0(), + OpenTelemetrySchemaVersion.V1_37_0 => new TelemetryProviderV1_37_0(), + _ => throw new InvalidOperationException($"Unsupported OpenTelemetry schema version: {version}") + }; + } + + + public Activity? StartActivityForInvocation(FunctionContext context) + { + if (!_source.HasListeners()) + { + return null; + } + + ActivityContext.TryParse( + context.TraceContext.TraceParent, + context.TraceContext.TraceState, + out var parent); + + // If there is no parent, we still want to create a new root activity. + return _source.StartActivity( + TraceConstants.ActivityAttributes.InvokeActivityName, + Kind, + parent, + tags: GetTelemetryAttributes(context)!); + } + + public virtual IEnumerable> GetTelemetryAttributes(FunctionContext context) + { + // Live-logs session + if (context.TraceContext.Attributes.TryGetValue(TraceConstants.InternalKeys.AzFuncLiveLogsSessionId, out var liveId) + && !string.IsNullOrWhiteSpace(liveId)) + { + yield return new(TraceConstants.InternalKeys.AzFuncLiveLogsSessionId, liveId); + } + } + + /// + /// Maps only known version strings to the enum. + /// If the string is anything else (and was explicitly set), we throw. + /// + private static OpenTelemetrySchemaVersion ParseSchemaVersion(string version) + { + return version switch + { + "1.17.0" => OpenTelemetrySchemaVersion.V1_17_0, + "1.37.0" => OpenTelemetrySchemaVersion.V1_37_0, + _ => throw new ArgumentException( + $"Invalid OpenTelemetry schema version '{version}'. ", nameof(version)) + }; + } +} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs deleted file mode 100644 index f8ea7b584..000000000 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics; - -namespace Microsoft.Azure.Functions.Worker.Diagnostics; - -internal abstract class TelemetryProviderBase : IFunctionTelemetryProvider -{ - private static readonly ActivitySource _source - = new(TraceConstants.FunctionsActivitySource, TraceConstants.FunctionsActivitySourceVersion); - - protected abstract OpenTelemetrySchemaVersion SchemaVersion { get; } - - protected abstract ActivityKind Kind { get; } - - public Activity? StartActivityForInvocation(FunctionContext context) - { - if (!_source.HasListeners()) - { - return null; - } - - ActivityContext.TryParse( - context.TraceContext.TraceParent, - context.TraceContext.TraceState, - out var parent); - - // If there is no parent, we still want to create a new root activity. - return _source.StartActivity( - TraceConstants.FunctionsInvokeActivityName, - Kind, - parent, - tags: GetTelemetryAttributes(context)!); - } - - public IEnumerable> GetTelemetryAttributes(FunctionContext context) - { - // Live-logs session - if (context.TraceContext.Attributes.TryGetValue(TraceConstants.AzFuncLiveLogsSessionIdKey, out var liveId) - && !string.IsNullOrWhiteSpace(liveId)) - { - yield return new(TraceConstants.AzFuncLiveLogsSessionIdKey, liveId); - } - - // Version-specific tags - foreach (var kv in GetVersionSpecificAttributes(context)) - { - yield return kv; - } - } - - protected abstract IEnumerable> GetVersionSpecificAttributes(FunctionContext context); -} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs index 71bbcde00..3355fcc35 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs @@ -6,19 +6,27 @@ namespace Microsoft.Azure.Functions.Worker.Diagnostics; -internal sealed class TelemetryProviderV1_17_0 : TelemetryProviderBase +internal sealed class TelemetryProviderV1_17_0 : TelemetryProvider { + private static readonly KeyValuePair SchemaUrlAttribute = + new(TraceConstants.OTelAttributes_1_17_0.SchemaUrl, TraceConstants.OTelAttributes_1_17_0.SchemaVersion); + protected override OpenTelemetrySchemaVersion SchemaVersion => OpenTelemetrySchemaVersion.V1_17_0; protected override ActivityKind Kind => ActivityKind.Server; - protected override IEnumerable> GetVersionSpecificAttributes(FunctionContext context) + public override IEnumerable> GetTelemetryAttributes(FunctionContext context) { - yield return new(TraceConstants.AttributeAzSchemaUrl, TraceConstants.OpenTelemetrySchemaMap[SchemaVersion]); - yield return new(TraceConstants.FunctionInvocationIdKey, context.InvocationId); - yield return new(TraceConstants.FunctionNameKey, context.FunctionDefinition.Name); - yield return new(TraceConstants.AttributeFaasExecution, context.InvocationId); + foreach (var kv in base.GetTelemetryAttributes(context)) + { + yield return kv; + } + + yield return SchemaUrlAttribute; + yield return new(TraceConstants.InternalKeys.FunctionInvocationId, context.InvocationId); + yield return new(TraceConstants.InternalKeys.FunctionName, context.FunctionDefinition.Name); + yield return new(TraceConstants.OTelAttributes_1_17_0.InvocationId, context.InvocationId); } } diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs index 2d3db654b..50dfa404f 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs @@ -6,24 +6,31 @@ namespace Microsoft.Azure.Functions.Worker.Diagnostics; -internal sealed class TelemetryProviderV1_37_0 : TelemetryProviderBase +internal sealed class TelemetryProviderV1_37_0 : TelemetryProvider { + private static readonly KeyValuePair SchemaUrlAttribute = + new(TraceConstants.OTelAttributes_1_37_0.SchemaUrl, TraceConstants.OTelAttributes_1_37_0.SchemaVersion); protected override OpenTelemetrySchemaVersion SchemaVersion => OpenTelemetrySchemaVersion.V1_37_0; protected override ActivityKind Kind => ActivityKind.Internal; - protected override IEnumerable> GetVersionSpecificAttributes(FunctionContext context) + public override IEnumerable> GetTelemetryAttributes(FunctionContext context) { - yield return new(TraceConstants.AttributeSchemaUrl, TraceConstants.OpenTelemetrySchemaMap[SchemaVersion]); - yield return new(TraceConstants.AttributeFaasInvocationId, context.InvocationId); - yield return new(TraceConstants.AttributeFaasFunctionName, context.FunctionDefinition.Name); + foreach (var kv in base.GetTelemetryAttributes(context)) + { + yield return kv; + } + + yield return SchemaUrlAttribute; + yield return new(TraceConstants.OTelAttributes_1_37_0.InvocationId, context.InvocationId); + yield return new(TraceConstants.OTelAttributes_1_37_0.FunctionName, context.FunctionDefinition.Name); - if (context.TraceContext.Attributes.TryGetValue(TraceConstants.HostInstanceIdKey, out var host) + if (context.TraceContext.Attributes.TryGetValue(TraceConstants.InternalKeys.HostInstanceId, out var host) && !string.IsNullOrEmpty(host)) { - yield return new(TraceConstants.AttributeFaasInstance, host); - } + yield return new(TraceConstants.OTelAttributes_1_37_0.Instance, host); + } } } diff --git a/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs b/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs index d1d3bacf5..b89f6ace6 100644 --- a/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs +++ b/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs @@ -3,45 +3,55 @@ using System.Collections.Generic; -namespace Microsoft.Azure.Functions.Worker.Diagnostics +namespace Microsoft.Azure.Functions.Worker.Diagnostics; + +internal static class TraceConstants { - internal class TraceConstants + public static class ActivityAttributes + { + public static readonly string Version = typeof(ActivityAttributes).Assembly.GetName().Version?.ToString() ?? string.Empty; + public const string Name = "Microsoft.Azure.Functions.Worker"; + public const string InvokeActivityName = "Invoke"; + } + + public static class ExceptionAttributes + { + public const string EventName = "exception"; + public const string Type = "exception.type"; + public const string Message = "exception.message"; + public const string Stacktrace = "exception.stacktrace"; + public const string Escaped = "exception.escaped"; + } + + public static class OTelAttributes_1_17_0 + { + // v1.17.0 + public const string InvocationId = "faas.execution"; + public const string SchemaUrl = "az.schema_url"; + public const string SchemaVersion = "https://opentelemetry.io/schemas/1.17.0"; + } + + public static class OTelAttributes_1_37_0 + { + // v1.37.0 + public const string InvocationId = "faas.invocation_id"; + public const string FunctionName = "faas.name"; + public const string Instance = "faas.instance"; + public const string SchemaUrl = "schema.url"; + public const string SchemaVersion = "https://opentelemetry.io/schemas/1.37.0"; + } + + public static class InternalKeys + { + public const string FunctionInvocationId = "AzureFunctions_InvocationId"; + public const string FunctionName = "AzureFunctions_FunctionName"; + public const string HostInstanceId = "HostInstanceId"; + public const string AzFuncLiveLogsSessionId = "#AzFuncLiveLogsSessionId"; + } + + public static class CapabilityFlags { - public static IReadOnlyDictionary OpenTelemetrySchemaMap = - new Dictionary() - { - [OpenTelemetrySchemaVersion.V1_17_0] = "https://opentelemetry.io/schemas/1.17.0", - [OpenTelemetrySchemaVersion.V1_37_0] = "https://opentelemetry.io/schemas/1.37.0" - }; - - public static readonly string FunctionsActivitySourceVersion = typeof(TraceConstants).Assembly.GetName().Version?.ToString() ?? string.Empty; - public const string FunctionsActivitySource = "Microsoft.Azure.Functions.Worker"; - public const string FunctionsInvokeActivityName = "Invoke"; - - public const string AttributeExceptionEventName = "exception"; - public const string AttributeExceptionType = "exception.type"; - public const string AttributeExceptionMessage = "exception.message"; - public const string AttributeExceptionStacktrace = "exception.stacktrace"; - public const string AttributeExceptionEscaped = "exception.escaped"; - - // v1.17.0 attributes - public const string AttributeFaasExecution = "faas.execution"; - public const string AttributeAzSchemaUrl = "az.schema_url"; - - // v1.37.0 attributes - public const string AttributeFaasInvocationId = "faas.invocation_id"; - public const string AttributeFaasFunctionName = "faas.name"; - public const string AttributeFaasInstance = "faas.instance"; - public const string AttributeSchemaUrl = "schema.url"; - - // Internal keys used for mapping - internal const string FunctionInvocationIdKey = "AzureFunctions_InvocationId"; - internal const string FunctionNameKey = "AzureFunctions_FunctionName"; - internal const string HostInstanceIdKey = "HostInstanceId"; - internal const string AzFuncLiveLogsSessionIdKey = "#AzFuncLiveLogsSessionId"; - - // Capability variables - internal const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; - internal const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; + public const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; + public const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; } } diff --git a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs index 6def63b78..988767d7f 100644 --- a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs @@ -127,8 +127,7 @@ internal static IServiceCollection AddDefaultInputConvertersToWorkerOptions(this /// Adds function telemetry services to the specified . /// /// This method registers a singleton - /// implementation based on the OpenTelemetry schema version specified in the worker options. If the - /// "WorkerOpenTelemetryEnabled" capability is set to , the schema version is determined + /// implementation based on the OpenTelemetry schema version specified in the worker options. The schema version is determined /// from the "WorkerOpenTelemetrySchemaVersion" capability. If the schema version is unsupported, an is thrown. /// The to which the telemetry services are added. @@ -140,24 +139,8 @@ internal static IServiceCollection AddFunctionTelemetry(this IServiceCollection { WorkerOptions options = sp.GetRequiredService>().Value; - bool otelFlag = options.Capabilities.TryGetValue(TraceConstants.WorkerOTelEnabled, out var flag) - && bool.TryParse(flag, out bool value) && value; - - // Always start at the legacy default - OpenTelemetrySchemaVersion version = OpenTelemetrySchemaVersion.V1_17_0; - - // If OTEL is on and a schema-capability is present, parse it (or throw) - if (otelFlag && options.Capabilities.TryGetValue(TraceConstants.WorkerOTelSchemaVersion, out var sv)) - { - version = ParseSchemaVersion(sv); - } - - return version switch - { - OpenTelemetrySchemaVersion.V1_37_0 => new TelemetryProviderV1_37_0(), - OpenTelemetrySchemaVersion.V1_17_0 => new TelemetryProviderV1_17_0(), - _ => throw new InvalidOperationException($"Unsupported OpenTelemetry schema version: {version}") - }; + options.Capabilities.TryGetValue(TraceConstants.CapabilityFlags.WorkerOTelSchemaVersion, out var schemaVersion); + return TelemetryProvider.Create(schemaVersion); }); return services; @@ -186,22 +169,7 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b var startupCodeExecutorInstance = Activator.CreateInstance(startupCodeExecutorInfoAttr.StartupCodeExecutorType) as WorkerExtensionStartup; startupCodeExecutorInstance!.Configure(builder); - } - - /// - /// Maps only known version strings to the enum. - /// If the string is anything else (and was explicitly set), we throw. - /// - private static OpenTelemetrySchemaVersion ParseSchemaVersion(string version) - { - return version switch - { - "1.17.0" => OpenTelemetrySchemaVersion.V1_17_0, - "1.37.0" => OpenTelemetrySchemaVersion.V1_37_0, - _ => throw new ArgumentException( - $"Invalid OpenTelemetry schema version '{version}'. ", nameof(version)) - }; - } + } private sealed class WorkerOptionsSetup(IOptions serializerOptions) : IPostConfigureOptions { diff --git a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs index aecd71d22..84245fe6a 100644 --- a/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs +++ b/src/DotNetWorker.Grpc/GrpcFunctionsHostLogWriter.cs @@ -88,7 +88,7 @@ private RpcLog AppendInvocationIdToLog(RpcLog rpcLog, IExternalScopeProvider sco { foreach (var pair in properties) { - if (pair.Key == TraceConstants.FunctionInvocationIdKey) + if (pair.Key == TraceConstants.InternalKeys.FunctionInvocationId) { log.InvocationId = pair.Value?.ToString(); break; diff --git a/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs b/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs index b78fd3a86..0e5d04328 100644 --- a/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs +++ b/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs @@ -19,7 +19,7 @@ public static IOpenTelemetryBuilder UseFunctionsWorkerDefaults(this IOpenTelemet builder.Services // Tells the host to no longer emit telemetry on behalf of the worker. .Configure(workerOptions => workerOptions.Capabilities[OpenTelemetryConstants.WorkerOTelEnabled] = bool.TrueString) - .Configure(workerOptions => workerOptions.Capabilities[OpenTelemetryConstants.WorkerOTelSchemaVersion] = OpenTelemetryConstants.WorkerSchemaVersion); + .Configure(workerOptions => workerOptions.Capabilities[OpenTelemetryConstants.WorkerOTelSchemaVersion] = OpenTelemetryConstants.WorkerDefaultSchemaVersion); builder.ConfigureResource((resourceBuilder) => { diff --git a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs index 01d6ae3f0..e6124d7e3 100644 --- a/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs +++ b/src/DotNetWorker.OpenTelemetry/OpenTelemetryConstants.cs @@ -12,7 +12,7 @@ internal class OpenTelemetryConstants internal const string RegionNameEnvVar = "REGION_NAME"; internal const string ResourceGroupEnvVar = "WEBSITE_RESOURCE_GROUP"; internal const string OwnerNameEnvVar = "WEBSITE_OWNER_NAME"; - internal const string WorkerSchemaVersion = "1.37.0"; + internal const string WorkerDefaultSchemaVersion = "1.37.0"; internal const string WorkerActivitySourceName = "Microsoft.Azure.Functions.Worker"; // Capability variables diff --git a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs index 2ce9d926a..d4dc58249 100644 --- a/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs +++ b/test/DotNetWorker.OpenTelemetry.Tests/EndToEndTests.cs @@ -50,7 +50,7 @@ private IHost InitializeHost(string schemaVersion = null) if (schemaVersion is not null) { options.Capabilities["WorkerOpenTelemetrySchemaVersion"] = schemaVersion; - } + } }) .Build(); @@ -91,7 +91,7 @@ public async Task ContextPropagation() Assert.Equal(activity.TraceId, activityContext.TraceId); Assert.Equal(activity.TraceStateString, activityContext.TraceState); Assert.Equal(ActivityKind.Internal, activity.Kind); - Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AttributeFaasInvocationId && t.Value == context.InvocationId); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.OTelAttributes_1_37_0.InvocationId && t.Value == context.InvocationId); } else { @@ -114,8 +114,8 @@ public async Task ContextPropagationV17() Assert.Equal(activity.TraceId, activityContext.TraceId); Assert.Equal(activity.TraceStateString, activityContext.TraceState); Assert.Equal(ActivityKind.Server, activity.Kind); - Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AttributeFaasExecution && t.Value == context.InvocationId); - Assert.Contains(activity.Tags, t => t.Key == TraceConstants.AzFuncLiveLogsSessionIdKey && t.Value == context.TraceContext.Attributes[TraceConstants.AzFuncLiveLogsSessionIdKey]); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.OTelAttributes_1_17_0.InvocationId && t.Value == context.InvocationId); + Assert.Contains(activity.Tags, t => t.Key == TraceConstants.InternalKeys.AzFuncLiveLogsSessionId && t.Value == context.TraceContext.Attributes[TraceConstants.InternalKeys.AzFuncLiveLogsSessionId]); } else { diff --git a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs index b1b4a9900..ed7144bf2 100644 --- a/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs +++ b/test/DotNetWorkerTests/ApplicationInsights/EndToEndTests.cs @@ -205,11 +205,11 @@ private static void ValidateDependencyTelemetry(DependencyTelemetry dependency, { Assert.Equal("CustomValue", dependency.Properties["CustomKey"]); - Assert.Equal(TraceConstants.FunctionsInvokeActivityName, dependency.Name); + Assert.Equal(TraceConstants.ActivityAttributes.InvokeActivityName, dependency.Name); Assert.Equal(activity.RootId, dependency.Context.Operation.Id); - Assert.Equal(context.InvocationId, dependency.Properties[TraceConstants.AttributeFaasExecution]); - Assert.Contains(TraceConstants.AttributeAzSchemaUrl, dependency.Properties.Keys); + Assert.Equal(context.InvocationId, dependency.Properties[TraceConstants.OTelAttributes_1_17_0.InvocationId]); + Assert.Contains(TraceConstants.OTelAttributes_1_17_0.SchemaUrl, dependency.Properties.Keys); ValidateCommonTelemetry(dependency); } @@ -220,8 +220,8 @@ private static void ValidateTraceTelemetry(TraceTelemetry trace, FunctionContext Assert.Equal(SeverityLevel.Warning, trace.SeverityLevel); // Check that scopes show up by default - Assert.Equal("TestName", trace.Properties[TraceConstants.FunctionNameKey]); - Assert.Equal(context.InvocationId, trace.Properties[TraceConstants.FunctionInvocationIdKey]); + Assert.Equal("TestName", trace.Properties[TraceConstants.InternalKeys.FunctionName]); + Assert.Equal(context.InvocationId, trace.Properties[TraceConstants.InternalKeys.FunctionInvocationId]); Assert.Equal(activity.RootId, trace.Context.Operation.Id); diff --git a/test/DotNetWorkerTests/FunctionsApplicationTests.cs b/test/DotNetWorkerTests/FunctionsApplicationTests.cs index a97720876..9d736956e 100644 --- a/test/DotNetWorkerTests/FunctionsApplicationTests.cs +++ b/test/DotNetWorkerTests/FunctionsApplicationTests.cs @@ -123,7 +123,7 @@ private static ActivityListener CreateListener(Action onStopped) { var listener = new ActivityListener { - ShouldListenTo = source => source.Name.StartsWith(TraceConstants.FunctionsActivitySource), + ShouldListenTo = source => source.Name.StartsWith(TraceConstants.ActivityAttributes.Name), ActivityStarted = activity => { }, ActivityStopped = onStopped, Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, diff --git a/test/TestUtility/TestFunctionInvocation.cs b/test/TestUtility/TestFunctionInvocation.cs index 0a8f3130b..85b4ffe42 100644 --- a/test/TestUtility/TestFunctionInvocation.cs +++ b/test/TestUtility/TestFunctionInvocation.cs @@ -26,8 +26,8 @@ public TestFunctionInvocation(string id = null, string functionId = null) using Activity activity = new Activity("Test").Start(); Dictionary attributes = new Dictionary { - { TraceConstants.FunctionInvocationIdKey, Guid.NewGuid().ToString() }, - { TraceConstants.AzFuncLiveLogsSessionIdKey, Guid.NewGuid().ToString() }, + { TraceConstants.InternalKeys.FunctionInvocationId, Guid.NewGuid().ToString() }, + { TraceConstants.InternalKeys.AzFuncLiveLogsSessionId, Guid.NewGuid().ToString() }, }; TraceContext = new DefaultTraceContext(activity.Id, Guid.NewGuid().ToString(), attributes); From f5002ad1cf16b1fc6e0a432d6f29ab6972a1caa5 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:41:23 -0700 Subject: [PATCH 5/7] Updating default OpenTelemetrySchemaVersion to 1.37 --- .../Telemetry/TelemetryProvider.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs index 0ed8af1f6..645490838 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs @@ -16,18 +16,29 @@ private static readonly ActivitySource _source protected abstract ActivityKind Kind { get; } - + /// + /// Creates a telemetry provider based on the provided schema version string. + /// Returns the default (1.17.0) if no version is provided. + /// + /// + /// public static TelemetryProvider Create(string? schema = null) { if (string.IsNullOrWhiteSpace(schema)) { - return Create(OpenTelemetrySchemaVersion.V1_17_0); + return Create(OpenTelemetrySchemaVersion.V1_37_0); } var version = ParseSchemaVersion(schema!); return Create(version); } + /// + /// Returns a telemetry provider for the specified version. + /// + /// + /// + /// public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) { return version switch @@ -38,7 +49,11 @@ public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) }; } - + /// + /// Starts an activity for the function invocation. + /// + /// + /// public Activity? StartActivityForInvocation(FunctionContext context) { if (!_source.HasListeners()) @@ -59,6 +74,11 @@ public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) tags: GetTelemetryAttributes(context)!); } + /// + /// Returns common telemetry attributes for a schema versions. + /// + /// + /// public virtual IEnumerable> GetTelemetryAttributes(FunctionContext context) { // Live-logs session From 0f40ec60216642353bba43d6eb856dac238f3ff7 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:39:43 -0700 Subject: [PATCH 6/7] Setting default schemaversion to 1.17 --- .../Diagnostics/Telemetry/TelemetryProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs index 645490838..3eaf288de 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs @@ -18,7 +18,7 @@ private static readonly ActivitySource _source /// /// Creates a telemetry provider based on the provided schema version string. - /// Returns the default (1.17.0) if no version is provided. + /// Returns the default (1.37.0) if no version is provided. /// /// /// @@ -26,7 +26,7 @@ public static TelemetryProvider Create(string? schema = null) { if (string.IsNullOrWhiteSpace(schema)) { - return Create(OpenTelemetrySchemaVersion.V1_37_0); + return Create(OpenTelemetrySchemaVersion.V1_17_0); } var version = ParseSchemaVersion(schema!); From 950b26f980622ec8f1ee07f5d3a5def9653a46b5 Mon Sep 17 00:00:00 2001 From: Rohit Ranjan <90008725+RohitRanjanMS@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:20:42 -0800 Subject: [PATCH 7/7] Addressing review comments. --- src/DotNetWorker.Core/Context/TraceContext.cs | 3 ++- .../Telemetry/IFunctionTelemetryProvider.cs | 9 +++++++-- .../Telemetry/TelemetryProvider.cs | 19 ++++++++++++------ .../Telemetry/TelemetryProviderV1_17_0.cs | 9 +++++++-- .../Telemetry/TelemetryProviderV1_37_0.cs | 20 +++++++++++++++++-- src/DotNetWorker.Core/FunctionsApplication.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 2 +- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/DotNetWorker.Core/Context/TraceContext.cs b/src/DotNetWorker.Core/Context/TraceContext.cs index ee1017217..8ffc1030a 100644 --- a/src/DotNetWorker.Core/Context/TraceContext.cs +++ b/src/DotNetWorker.Core/Context/TraceContext.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Collections.Immutable; namespace Microsoft.Azure.Functions.Worker { @@ -23,6 +24,6 @@ public abstract class TraceContext /// /// Gets the attributes associated with the trace. /// - public virtual IReadOnlyDictionary Attributes => new Dictionary(); + public virtual IReadOnlyDictionary Attributes => ImmutableDictionary.Empty; } } diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs index 4b856da6d..e9560ffb0 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs @@ -14,9 +14,14 @@ namespace Microsoft.Azure.Functions.Worker.Diagnostics; internal interface IFunctionTelemetryProvider { /// - /// Returns the attributes to be applied to the Activity/Scope for this invocation. + /// Returns the attributes to be applied to the Scope for this invocation. /// - IEnumerable> GetTelemetryAttributes(FunctionContext ctx); + IEnumerable> GetScopeAttributes(FunctionContext ctx); + + /// + /// Returns the attributes to be applied to the Activity for this invocation. + /// + IEnumerable> GetTagAttributes(FunctionContext ctx); /// /// Starts the Activity for this invocation. diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs index 3eaf288de..e1f962178 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs @@ -18,7 +18,7 @@ private static readonly ActivitySource _source /// /// Creates a telemetry provider based on the provided schema version string. - /// Returns the default (1.37.0) if no version is provided. + /// Returns the default (1.17.0) if no version is provided. /// /// /// @@ -38,14 +38,14 @@ public static TelemetryProvider Create(string? schema = null) /// /// /// - /// + /// public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) { return version switch { OpenTelemetrySchemaVersion.V1_17_0 => new TelemetryProviderV1_17_0(), OpenTelemetrySchemaVersion.V1_37_0 => new TelemetryProviderV1_37_0(), - _ => throw new InvalidOperationException($"Unsupported OpenTelemetry schema version: {version}") + _ => throw new ArgumentException($"Unsupported OpenTelemetry schema version: {version}") }; } @@ -71,15 +71,15 @@ public static TelemetryProvider Create(OpenTelemetrySchemaVersion version) TraceConstants.ActivityAttributes.InvokeActivityName, Kind, parent, - tags: GetTelemetryAttributes(context)!); + tags: GetTagAttributes(context)!); } /// - /// Returns common telemetry attributes for a schema versions. + /// Returns common scope attributes for a schema versions. /// /// /// - public virtual IEnumerable> GetTelemetryAttributes(FunctionContext context) + public virtual IEnumerable> GetScopeAttributes(FunctionContext context) { // Live-logs session if (context.TraceContext.Attributes.TryGetValue(TraceConstants.InternalKeys.AzFuncLiveLogsSessionId, out var liveId) @@ -89,6 +89,13 @@ public virtual IEnumerable> GetTelemetryAttributes( } } + /// + /// Returns common tag attributes for a schema versions. + /// + /// + /// + public abstract IEnumerable> GetTagAttributes(FunctionContext context); + /// /// Maps only known version strings to the enum. /// If the string is anything else (and was explicitly set), we throw. diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs index 3355fcc35..1e5517253 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs @@ -17,9 +17,9 @@ protected override OpenTelemetrySchemaVersion SchemaVersion protected override ActivityKind Kind => ActivityKind.Server; - public override IEnumerable> GetTelemetryAttributes(FunctionContext context) + public override IEnumerable> GetScopeAttributes(FunctionContext context) { - foreach (var kv in base.GetTelemetryAttributes(context)) + foreach (var kv in base.GetScopeAttributes(context)) { yield return kv; } @@ -27,6 +27,11 @@ public override IEnumerable> GetTelemetryAttributes yield return SchemaUrlAttribute; yield return new(TraceConstants.InternalKeys.FunctionInvocationId, context.InvocationId); yield return new(TraceConstants.InternalKeys.FunctionName, context.FunctionDefinition.Name); + } + + public override IEnumerable> GetTagAttributes(FunctionContext context) + { + yield return SchemaUrlAttribute; yield return new(TraceConstants.OTelAttributes_1_17_0.InvocationId, context.InvocationId); } } diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs index 50dfa404f..6ad9e4294 100644 --- a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs @@ -16,13 +16,29 @@ protected override OpenTelemetrySchemaVersion SchemaVersion protected override ActivityKind Kind => ActivityKind.Internal; - public override IEnumerable> GetTelemetryAttributes(FunctionContext context) + public override IEnumerable> GetScopeAttributes(FunctionContext context) { - foreach (var kv in base.GetTelemetryAttributes(context)) + foreach (var kv in base.GetScopeAttributes(context)) { yield return kv; } + foreach (var kv in GetCommonAttributes(context)) + { + yield return kv; + } + } + + public override IEnumerable> GetTagAttributes(FunctionContext context) + { + foreach (var kv in GetCommonAttributes(context)) + { + yield return kv; + } + } + + private IEnumerable> GetCommonAttributes(FunctionContext context) + { yield return SchemaUrlAttribute; yield return new(TraceConstants.OTelAttributes_1_37_0.InvocationId, context.InvocationId); yield return new(TraceConstants.OTelAttributes_1_37_0.FunctionName, context.FunctionDefinition.Name); diff --git a/src/DotNetWorker.Core/FunctionsApplication.cs b/src/DotNetWorker.Core/FunctionsApplication.cs index b4cbeb347..40157f9c0 100644 --- a/src/DotNetWorker.Core/FunctionsApplication.cs +++ b/src/DotNetWorker.Core/FunctionsApplication.cs @@ -68,7 +68,7 @@ public void LoadFunction(FunctionDefinition definition) public async Task InvokeFunctionAsync(FunctionContext context) { - using var logScope = _logger.BeginScope(_functionTelemetryProvider.GetTelemetryAttributes(context).ToList()); + using var logScope = _logger.BeginScope(_functionTelemetryProvider.GetScopeAttributes(context).ToList()); using Activity? invokeActivity = _functionTelemetryProvider.StartActivityForInvocation(context); try diff --git a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs index 988767d7f..1cac29ae1 100644 --- a/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs +++ b/src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs @@ -169,7 +169,7 @@ private static void RunExtensionStartupCode(IFunctionsWorkerApplicationBuilder b var startupCodeExecutorInstance = Activator.CreateInstance(startupCodeExecutorInfoAttr.StartupCodeExecutorType) as WorkerExtensionStartup; startupCodeExecutorInstance!.Configure(builder); - } + } private sealed class WorkerOptionsSetup(IOptions serializerOptions) : IPostConfigureOptions {