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..8ffc1030a 100644 --- a/src/DotNetWorker.Core/Context/TraceContext.cs +++ b/src/DotNetWorker.Core/Context/TraceContext.cs @@ -1,6 +1,9 @@ -// 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; +using System.Collections.Immutable; + namespace Microsoft.Azure.Functions.Worker { /// @@ -17,5 +20,10 @@ public abstract class TraceContext /// Gets the state data. /// public abstract string TraceState { get; } + + /// + /// Gets the attributes associated with the trace. + /// + public virtual IReadOnlyDictionary Attributes => ImmutableDictionary.Empty; } } 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..e9560ffb0 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/IFunctionTelemetryProvider.cs @@ -0,0 +1,30 @@ +// 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 Scope for this invocation. + /// + 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. + /// + Activity? StartActivityForInvocation(FunctionContext ctx); +} diff --git a/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs new file mode 100644 index 000000000..e1f962178 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProvider.cs @@ -0,0 +1,113 @@ +// 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; } + + /// + /// 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); + } + + var version = ParseSchemaVersion(schema!); + return Create(version); + } + + /// + /// Returns a telemetry provider for the specified 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 ArgumentException($"Unsupported OpenTelemetry schema version: {version}") + }; + } + + /// + /// Starts an activity for the function invocation. + /// + /// + /// + 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: GetTagAttributes(context)!); + } + + /// + /// Returns common scope attributes for a schema versions. + /// + /// + /// + public virtual IEnumerable> GetScopeAttributes(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); + } + } + + /// + /// 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. + /// + 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/TelemetryProviderV1_17_0.cs b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs new file mode 100644 index 000000000..1e5517253 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_17_0.cs @@ -0,0 +1,37 @@ +// 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 : 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; + + public override IEnumerable> GetScopeAttributes(FunctionContext context) + { + foreach (var kv in base.GetScopeAttributes(context)) + { + yield return kv; + } + + 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 new file mode 100644 index 000000000..6ad9e4294 --- /dev/null +++ b/src/DotNetWorker.Core/Diagnostics/Telemetry/TelemetryProviderV1_37_0.cs @@ -0,0 +1,52 @@ +// 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 : 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; + + public override IEnumerable> GetScopeAttributes(FunctionContext 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); + + if (context.TraceContext.Attributes.TryGetValue(TraceConstants.InternalKeys.HostInstanceId, out var host) + && !string.IsNullOrEmpty(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 c8d7ecafc..b89f6ace6 100644 --- a/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs +++ b/src/DotNetWorker.Core/Diagnostics/TraceConstants.cs @@ -1,31 +1,57 @@ -// 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.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 const string FunctionsActivitySource = "Microsoft.Azure.Functions.Worker"; - public const string FunctionsActivitySourceVersion = "1.0.0.0"; - 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"; - - 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/ - public const string AttributeFaasExecution = "faas.execution"; + public const string WorkerOTelEnabled = "WorkerOpenTelemetryEnabled"; + public const string WorkerOTelSchemaVersion = "WorkerOpenTelemetrySchemaVersion"; } } diff --git a/src/DotNetWorker.Core/FunctionsApplication.cs b/src/DotNetWorker.Core/FunctionsApplication.cs index 11db9d003..40157f9c0 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.GetScopeAttributes(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..1cac29ae1 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,29 @@ 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. 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; + + options.Capabilities.TryGetValue(TraceConstants.CapabilityFlags.WorkerOTelSchemaVersion, out var schemaVersion); + return TelemetryProvider.Create(schemaVersion); + }); + + return services; + } + /// /// Run extension startup execution code. /// Our source generator creates a class(WorkerExtensionStartupCodeExecutor) 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..84245fe6a 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.InternalKeys.FunctionInvocationId) { log.InvocationId = pair.Value?.ToString(); break; diff --git a/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs b/src/DotNetWorker.OpenTelemetry/ConfigureFunctionsOpenTelemetry.cs index a31ab6e8a..0e5d04328 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.WorkerDefaultSchemaVersion); 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..e6124d7e3 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,11 @@ 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 WorkerDefaultSchemaVersion = "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..d4dc58249 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,6 +79,7 @@ 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); await _application.InvokeFunctionAsync(context); @@ -79,12 +87,11 @@ public async Task ContextPropagation() 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.OTelAttributes_1_37_0.InvocationId && t.Value == context.InvocationId); } else { @@ -92,6 +99,60 @@ 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.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 + { + 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 ContextPropagation_EmptyVersion() + { + try + { + using var host = InitializeHost(string.Empty); + var context = CreateContext(host); + await _application.InvokeFunctionAsync(context); + } + catch (Exception ex) + { + Assert.IsType(ex); + } + } + [Fact] public async Task ContextPropagationWithTriggerInstrumentation() { @@ -129,7 +190,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 +259,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..ed7144bf2 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; @@ -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.AttributeSchemaUrl, 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[FunctionInvocationScope.FunctionNameKey]); - Assert.Equal(context.InvocationId, trace.Properties[FunctionInvocationScope.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/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..9d736956e 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; @@ -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,16 +114,16 @@ 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) { 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 2004d5e61..85b4ffe42 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.InternalKeys.FunctionInvocationId, Guid.NewGuid().ToString() }, + { TraceConstants.InternalKeys.AzFuncLiveLogsSessionId, Guid.NewGuid().ToString() }, + }; + + TraceContext = new DefaultTraceContext(activity.Id, Guid.NewGuid().ToString(), attributes); } public override string Id { get; } = Guid.NewGuid().ToString();