diff --git a/src/All.slnx b/src/All.slnx
index f24b874c56f..6e47fb35693 100644
--- a/src/All.slnx
+++ b/src/All.slnx
@@ -212,6 +212,7 @@
+
@@ -222,6 +223,7 @@
+
diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
index 10e92743932..41b16cac147 100644
--- a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
+++ b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
@@ -126,8 +126,7 @@ public virtual void EnrichSingleRequest(
if (request.Variables is not null
&& (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
{
- var node = CreateVariablesNode(request.Variables);
- EnrichRequestVariables(context, request, node, activity);
+ EnrichRequestVariables(context, request, request.Variables, activity);
}
if (request.Extensions is not null
@@ -175,8 +174,7 @@ public virtual void EnrichBatchRequest(
if (request.Variables is not null
&& (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
{
- var node = CreateVariablesNode(request.Variables);
- EnrichBatchVariables(context, request, node, i, activity);
+ EnrichBatchVariables(context, request, request.Variables, i, activity);
}
if (request.Extensions is not null
@@ -222,8 +220,7 @@ public virtual void EnrichOperationBatchRequest(
if (request.Variables is not null
&& (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
{
- var node = CreateVariablesNode(request.Variables);
- EnrichRequestVariables(context, request, node, activity);
+ EnrichRequestVariables(context, request, request.Variables, activity);
}
if (request.Extensions is not null
@@ -236,17 +233,17 @@ public virtual void EnrichOperationBatchRequest(
protected virtual void EnrichRequestVariables(
HttpContext context,
GraphQLRequest request,
- ISyntaxNode variables,
+ JsonDocument variables,
Activity activity)
- => activity.SetTag("graphql.http.request.variables", variables.Print(indented: false));
+ => activity.SetTag("graphql.http.request.variables", variables.RootElement.ToString());
protected virtual void EnrichBatchVariables(
HttpContext context,
GraphQLRequest request,
- ISyntaxNode variables,
+ JsonDocument variables,
int index,
Activity activity)
- => activity.SetTag($"graphql.http.request[{index}].variables", variables.Print(indented: false));
+ => activity.SetTag($"graphql.http.request[{index}].variables", variables.RootElement.ToString());
protected virtual void EnrichRequestExtensions(
HttpContext context,
@@ -616,24 +613,6 @@ protected virtual void EnrichError(IError error, Activity activity)
activity.AddEvent(new ActivityEvent(AttributeExceptionEventName, default, tags));
}
-
- private static ISyntaxNode CreateVariablesNode(JsonDocument? variables)
- {
- if (variables is null)
- {
- return NullValueNode.Default;
- }
-
- var root = variables.RootElement;
-
- if (root.ValueKind is not (JsonValueKind.Object or JsonValueKind.Array))
- {
- throw new InvalidOperationException();
- }
-
- var parser = new JsonValueParser();
- return parser.Parse(root);
- }
}
file static class SemanticConventions
diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
index 73169370f2a..47493fa3a0b 100644
--- a/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
@@ -80,13 +80,8 @@ public static IRequestExecutorBuilder AddInstrumentation(
return builder;
}
- private sealed class InternalActivityEnricher : ActivityEnricher
- {
- public InternalActivityEnricher(
- ObjectPool stringBuilderPool,
- InstrumentationOptions options)
- : base(stringBuilderPool, options)
- {
- }
- }
+ private sealed class InternalActivityEnricher(
+ ObjectPool stringBuilderPool,
+ InstrumentationOptions options)
+ : ActivityEnricher(stringBuilderPool, options);
}
diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ServerInstrumentationTests.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ServerInstrumentationTests.cs
index 073c270cdb1..bc07c2f38fb 100644
--- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ServerInstrumentationTests.cs
+++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ServerInstrumentationTests.cs
@@ -153,7 +153,7 @@ public async Task Http_Post_add_query_to_http_activity()
o =>
{
o.Scopes = ActivityScopes.All;
- o.RequestDetails = RequestDetails.Default | RequestDetails.Variables;
+ o.RequestDetails = RequestDetails.Default | RequestDetails.Operation;
});
// act
diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_query_to_http_activity.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_query_to_http_activity.snap
index 039b08ef442..ed74db6956e 100644
--- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_query_to_http_activity.snap
+++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_query_to_http_activity.snap
@@ -12,10 +12,6 @@
{
"Key": "graphql.http.request.type",
"Value": "single"
- },
- {
- "Key": "graphql.http.request.variables",
- "Value": "{ episode: \"NEW_HOPE\" }"
}
],
"event": [],
diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_variables_to_http_activity.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_variables_to_http_activity.snap
index 039b08ef442..c6b48eaef41 100644
--- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_variables_to_http_activity.snap
+++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_add_variables_to_http_activity.snap
@@ -15,7 +15,7 @@
},
{
"Key": "graphql.http.request.variables",
- "Value": "{ episode: \"NEW_HOPE\" }"
+ "Value": "{\"episode\":\"NEW_HOPE\"}"
}
],
"event": [],
diff --git a/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx b/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx
index 9e2a631d796..077a6e20e93 100644
--- a/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx
+++ b/src/HotChocolate/Fusion-vnext/HotChocolate.Fusion-vnext.slnx
@@ -6,6 +6,7 @@
+
@@ -16,6 +17,7 @@
+
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/ContextKeys.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/ContextKeys.cs
new file mode 100644
index 00000000000..f35bd4946f0
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/ContextKeys.cs
@@ -0,0 +1,10 @@
+namespace HotChocolate.Fusion.Diagnostics;
+
+internal static class ContextKeys
+{
+ public const string HttpRequestActivity = "HotChocolate.Fusion.Diagnostics.HttpRequest";
+ public const string ParseHttpRequestActivity = "HotChocolate.Fusion.Diagnostics.ParseHttpRequest";
+ public const string FormatHttpResponseActivity = "HotChocolate.Fusion.Diagnostics.FormatHttpResponse";
+ public const string RequestActivity = "HotChocolate.Fusion.Diagnostics.Request";
+ public const string ValidateActivity = "HotChocolate.Fusion.Diagnostics.Validate";
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs
new file mode 100644
index 00000000000..689f319827a
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs
@@ -0,0 +1,56 @@
+using System.Text;
+using HotChocolate.Fusion.Diagnostics;
+using HotChocolate.Fusion.Diagnostics.Listeners;
+using HotChocolate.Fusion.Configuration;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.ObjectPool;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class DiagnosticsFusionGatewayBuilderExtensions
+{
+ public static IFusionGatewayBuilder AddInstrumentation(
+ this IFusionGatewayBuilder builder,
+ Action? options = null)
+ => AddInstrumentation(builder, (_, opt) => options?.Invoke(opt));
+
+ public static IFusionGatewayBuilder AddInstrumentation(
+ this IFusionGatewayBuilder builder,
+ Action options)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(options);
+
+ builder.Services.TryAddSingleton(
+ sp =>
+ {
+ var optionInst = new InstrumentationOptions();
+ options(sp, optionInst);
+ return optionInst;
+ });
+
+ builder.Services.TryAddSingleton();
+
+ builder.AddApplicationService();
+ builder.AddApplicationService();
+
+ builder.AddDiagnosticEventListener(
+ sp => new ActivityFusionExecutionDiagnosticEventListener(
+ sp.GetService() ??
+ sp.GetRequiredService(),
+ sp.GetRequiredService()));
+
+ builder.AddDiagnosticEventListener(
+ sp => new ActivityServerDiagnosticListener(
+ sp.GetService() ??
+ sp.GetRequiredService(),
+ sp.GetRequiredService()));
+
+ return builder;
+ }
+
+ private sealed class InternalActivityEnricher(
+ ObjectPool stringBuilderPool,
+ InstrumentationOptions options)
+ : FusionActivityEnricher(stringBuilderPool, options);
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/TracerProviderBuilderExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/TracerProviderBuilderExtensions.cs
new file mode 100644
index 00000000000..e02cee949d2
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Extensions/TracerProviderBuilderExtensions.cs
@@ -0,0 +1,27 @@
+using HotChocolate.Fusion.Diagnostics;
+
+namespace OpenTelemetry.Trace;
+
+///
+/// Provides configuration methods to open-telemetry.
+///
+public static class TracerProviderBuilderExtensions
+{
+ ///
+ /// Adds the Hot Chocolate Fusion instrumentation to open-telemetry.
+ ///
+ ///
+ /// The tracing builder.
+ ///
+ ///
+ /// Returns the tracing builder for configuration chaining.
+ ///
+ public static TracerProviderBuilder AddHotChocolateFusionInstrumentation(
+ this TracerProviderBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ builder.AddSource(HotChocolateFusionActivitySource.GetName());
+ return builder;
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityEnricher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityEnricher.cs
new file mode 100644
index 00000000000..03839bc31c9
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityEnricher.cs
@@ -0,0 +1,592 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.ObjectPool;
+using HotChocolate.AspNetCore.Instrumentation;
+using HotChocolate.Execution;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using HotChocolate.Language;
+using HotChocolate.Language.Utilities;
+using OpenTelemetry.Trace;
+using static HotChocolate.Fusion.Diagnostics.SemanticConventions;
+using static HotChocolate.WellKnownContextData;
+
+namespace HotChocolate.Fusion.Diagnostics;
+
+///
+/// The activity enricher is used to add information to the activity spans.
+/// You can inherit from this class and override the enricher methods to provide more or
+/// less information.
+///
+public class FusionActivityEnricher
+{
+ private readonly InstrumentationOptions _options;
+ private readonly ConditionalWeakTable _queryCache = [];
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ ///
+ ///
+ protected FusionActivityEnricher(
+ ObjectPool stringBuilderPool,
+ InstrumentationOptions options)
+ {
+ StringBuilderPool = stringBuilderPool;
+ _options = options;
+ }
+
+ ///
+ /// Gets the pool used by this enricher.
+ ///
+ protected ObjectPool StringBuilderPool { get; }
+
+ public virtual void EnrichExecuteHttpRequest(
+ HttpContext context,
+ HttpRequestKind kind,
+ Activity activity)
+ {
+ switch (kind)
+ {
+ case HttpRequestKind.HttpPost:
+ activity.DisplayName = "GraphQL HTTP POST";
+ break;
+ case HttpRequestKind.HttpMultiPart:
+ activity.DisplayName = "GraphQL HTTP POST MultiPart";
+ break;
+ case HttpRequestKind.HttpGet:
+ activity.DisplayName = "GraphQL HTTP GET";
+ break;
+ case HttpRequestKind.HttpGetSchema:
+ activity.DisplayName = "GraphQL HTTP GET SDL";
+ break;
+ }
+
+ if (_options.RenameRootActivity)
+ {
+ UpdateRootActivityName(activity, $"Begin {activity.DisplayName}");
+ }
+
+ activity.SetTag("graphql.http.kind", kind);
+
+ var isDefault = false;
+ if (!(context.Items.TryGetValue(SchemaName, out var value)
+ && value is string schemaName))
+ {
+ schemaName = ISchemaDefinition.DefaultName;
+ isDefault = true;
+ }
+
+ activity.SetTag("graphql.schema.name", schemaName);
+ activity.SetTag("graphql.schema.isDefault", isDefault);
+ }
+
+ public virtual void EnrichSingleRequest(
+ HttpContext context,
+ GraphQLRequest request,
+ Activity activity)
+ {
+ activity.SetTag("graphql.http.request.type", "single");
+
+ if (request.DocumentId is not null
+ && (_options.RequestDetails & RequestDetails.Id) == RequestDetails.Id)
+ {
+ activity.SetTag("graphql.http.request.query.id", request.DocumentId.Value);
+ }
+
+ if (request.DocumentHash is not null
+ && (_options.RequestDetails & RequestDetails.Hash) == RequestDetails.Hash)
+ {
+ activity.SetTag("graphql.http.request.query.hash", request.DocumentHash.Value);
+ }
+
+ if (request.Document is not null
+ && (_options.RequestDetails & RequestDetails.Query) == RequestDetails.Query)
+ {
+ if (!_queryCache.TryGetValue(request.Document, out var query))
+ {
+ query = request.Document.Print();
+ _queryCache.Add(request.Document, query);
+ }
+
+ activity.SetTag("graphql.http.request.query.body", query);
+ }
+
+ if (request.OperationName is not null
+ && (_options.RequestDetails & RequestDetails.Operation) == RequestDetails.Operation)
+ {
+ activity.SetTag("graphql.http.request.operation", request.OperationName);
+ }
+
+ if (request.Variables is not null
+ && (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
+ {
+ EnrichRequestVariables(context, request, request.Variables, activity);
+ }
+
+ if (request.Extensions is not null
+ && (_options.RequestDetails & RequestDetails.Extensions) == RequestDetails.Extensions)
+ {
+ EnrichRequestExtensions(context, request, request.Extensions, activity);
+ }
+ }
+
+ public virtual void EnrichBatchRequest(
+ HttpContext context,
+ IReadOnlyList batch,
+ Activity activity)
+ {
+ activity.SetTag("graphql.http.request.type", "batch");
+
+ for (var i = 0; i < batch.Count; i++)
+ {
+ var request = batch[i];
+
+ if (request.DocumentId is not null
+ && (_options.RequestDetails & RequestDetails.Id) == RequestDetails.Id)
+ {
+ activity.SetTag($"graphql.http.request[{i}].query.id", request.DocumentId.Value);
+ }
+
+ if (request.DocumentHash is not null
+ && (_options.RequestDetails & RequestDetails.Hash) == RequestDetails.Hash)
+ {
+ activity.SetTag($"graphql.http.request[{i}].query.hash", request.DocumentHash.Value);
+ }
+
+ if (request.Document is not null
+ && (_options.RequestDetails & RequestDetails.Query) == RequestDetails.Query)
+ {
+ activity.SetTag($"graphql.http.request[{i}].query.body", request.Document.Print());
+ }
+
+ if (request.OperationName is not null
+ && (_options.RequestDetails & RequestDetails.Operation) == RequestDetails.Operation)
+ {
+ activity.SetTag($"graphql.http.request[{i}].operation", request.OperationName);
+ }
+
+ if (request.Variables is not null
+ && (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
+ {
+ EnrichBatchVariables(context, request, request.Variables, i, activity);
+ }
+
+ if (request.Extensions is not null
+ && (_options.RequestDetails & RequestDetails.Extensions) == RequestDetails.Extensions)
+ {
+ EnrichBatchExtensions(context, request, request.Extensions, i, activity);
+ }
+ }
+ }
+
+ public virtual void EnrichOperationBatchRequest(
+ HttpContext context,
+ GraphQLRequest request,
+ IReadOnlyList operations,
+ Activity activity)
+ {
+ activity.SetTag("graphql.http.request.type", "operationBatch");
+
+ if (request.DocumentId is not null
+ && (_options.RequestDetails & RequestDetails.Id) == RequestDetails.Id)
+ {
+ activity.SetTag("graphql.http.request.query.id", request.DocumentId.Value);
+ }
+
+ if (request.DocumentHash is not null
+ && (_options.RequestDetails & RequestDetails.Hash) == RequestDetails.Hash)
+ {
+ activity.SetTag("graphql.http.request.query.hash", request.DocumentHash.Value);
+ }
+
+ if (request.Document is not null
+ && (_options.RequestDetails & RequestDetails.Query) == RequestDetails.Query)
+ {
+ activity.SetTag("graphql.http.request.query.body", request.Document.Print());
+ }
+
+ if (request.OperationName is not null
+ && (_options.RequestDetails & RequestDetails.Operation) == RequestDetails.Operation)
+ {
+ activity.SetTag("graphql.http.request.operations", string.Join(" -> ", operations));
+ }
+
+ if (request.Variables is not null
+ && (_options.RequestDetails & RequestDetails.Variables) == RequestDetails.Variables)
+ {
+ EnrichRequestVariables(context, request, request.Variables, activity);
+ }
+
+ if (request.Extensions is not null
+ && (_options.RequestDetails & RequestDetails.Extensions) == RequestDetails.Extensions)
+ {
+ EnrichRequestExtensions(context, request, request.Extensions, activity);
+ }
+ }
+
+ protected virtual void EnrichRequestVariables(
+ HttpContext context,
+ GraphQLRequest request,
+ JsonDocument variables,
+ Activity activity)
+ {
+ activity.SetTag("graphql.http.request.variables", variables.RootElement.ToString());
+ }
+
+ protected virtual void EnrichBatchVariables(
+ HttpContext context,
+ GraphQLRequest request,
+ JsonDocument variables,
+ int index,
+ Activity activity)
+ {
+ activity.SetTag($"graphql.http.request[{index}].variables", variables.RootElement.ToString());
+ }
+
+ protected virtual void EnrichRequestExtensions(
+ HttpContext context,
+ GraphQLRequest request,
+ JsonDocument extensions,
+ Activity activity)
+ {
+ try
+ {
+ activity.SetTag(
+ "graphql.http.request.extensions",
+ extensions.RootElement.ToString());
+ }
+ catch
+ {
+ // Ignore any errors
+ }
+ }
+
+ protected virtual void EnrichBatchExtensions(
+ HttpContext context,
+ GraphQLRequest request,
+ JsonDocument extensions,
+ int index,
+ Activity activity)
+ {
+ try
+ {
+ activity.SetTag(
+ $"graphql.http.request[{index}].extensions",
+ extensions.RootElement.ToString());
+ }
+ catch
+ {
+ // Ignore any errors
+ }
+ }
+
+ public virtual void EnrichHttpRequestError(
+ HttpContext context,
+ IError error,
+ Activity activity)
+ => EnrichError(error, activity);
+
+ public virtual void EnrichHttpRequestError(
+ HttpContext context,
+ Exception exception,
+ Activity activity)
+ {
+ }
+
+ public virtual void EnrichParseHttpRequest(HttpContext context, Activity activity)
+ {
+ activity.DisplayName = "Parse HTTP Request";
+
+ if (_options.RenameRootActivity)
+ {
+ UpdateRootActivityName(activity, $"Begin {activity.DisplayName}");
+ }
+ }
+
+ public virtual void EnrichParserErrors(HttpContext context, IError error, Activity activity)
+ => EnrichError(error, activity);
+
+ public virtual void EnrichFormatHttpResponse(HttpContext context, Activity activity)
+ {
+ activity.DisplayName = "Format HTTP Response";
+ }
+
+ public virtual void EnrichExecuteRequest(RequestContext context, Activity activity)
+ {
+ var plan = context.GetOperationPlan();
+ var documentInfo = context.OperationDocumentInfo;
+ var operationDisplayName = CreateOperationDisplayName(context, plan);
+
+ if (_options.RenameRootActivity && operationDisplayName is not null)
+ {
+ UpdateRootActivityName(activity, operationDisplayName);
+ }
+
+ activity.DisplayName = operationDisplayName ?? "Execute Request";
+ activity.SetTag("graphql.document.id", documentInfo.Id.Value);
+ activity.SetTag("graphql.document.hash", documentInfo.Hash.Value);
+ activity.SetTag("graphql.document.valid", documentInfo.IsValidated);
+ activity.SetTag("graphql.operation.id", plan?.Id);
+ activity.SetTag("graphql.operation.kind", plan?.Operation.Definition.Operation);
+ activity.SetTag("graphql.operation.name", plan?.OperationName);
+
+ if (_options.IncludeDocument && documentInfo.Document is not null)
+ {
+ activity.SetTag("graphql.document.body", documentInfo.Document.Print());
+ }
+
+ if (context.Result is OperationResult {Errors: [_, ..] errors})
+ {
+ activity.SetTag("graphql.errors.count", errors.Count);
+ }
+ }
+
+ protected virtual string? CreateOperationDisplayName(RequestContext context, OperationPlan? plan)
+ {
+ if (plan is null)
+ {
+ return null;
+ }
+
+ var displayName = StringBuilderPool.Get();
+
+ try
+ {
+ var rootSelectionSet = plan.Operation.RootSelectionSet;
+ var selectionCount = rootSelectionSet.Selections.Length;
+
+ displayName.Append('{');
+ displayName.Append(' ');
+
+ foreach (var selection in rootSelectionSet.Selections[..Math.Min(3, selectionCount)])
+ {
+ if (displayName.Length > 2)
+ {
+ displayName.Append(' ');
+ }
+
+ displayName.Append(selection.ResponseName);
+ }
+
+ if (rootSelectionSet.Selections.Length > 3)
+ {
+ displayName.Append(' ');
+ displayName.Append('.');
+ displayName.Append('.');
+ displayName.Append('.');
+ }
+
+ displayName.Append(' ');
+ displayName.Append('}');
+
+ if (plan.OperationName is { } name)
+ {
+ displayName.Insert(0, ' ');
+ displayName.Insert(0, name);
+ }
+
+ displayName.Insert(0, ' ');
+ displayName.Insert(0, plan.Operation.Definition.Operation.ToString().ToLowerInvariant());
+
+ return displayName.ToString();
+ }
+ finally
+ {
+ StringBuilderPool.Return(displayName);
+ }
+ }
+
+ private void UpdateRootActivityName(Activity activity, string displayName)
+ {
+ var current = activity;
+
+ while (current.Parent is not null)
+ {
+ current = current.Parent;
+ }
+
+ if (current != activity)
+ {
+ current.DisplayName = CreateRootActivityName(activity, current, displayName);
+ }
+ }
+
+ protected virtual string CreateRootActivityName(
+ Activity activity,
+ Activity root,
+ string displayName)
+ {
+ const string key = "originalDisplayName";
+
+ if (root.GetCustomProperty(key) is not string rootDisplayName)
+ {
+ rootDisplayName = root.DisplayName;
+ root.SetCustomProperty(key, rootDisplayName);
+ }
+
+ return $"{rootDisplayName}: {displayName}";
+ }
+
+ public virtual void EnrichParseDocument(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Parse Document";
+
+ if (_options.RenameRootActivity)
+ {
+ UpdateRootActivityName(activity, $"Begin {activity.DisplayName}");
+ }
+ }
+
+ public virtual void EnrichRequestError(
+ RequestContext context,
+ Activity activity,
+ Exception error)
+ => EnrichError(ErrorBuilder.FromException(error).Build(), activity);
+
+ public virtual void EnrichRequestError(
+ RequestContext context,
+ Activity activity,
+ IError error)
+ => EnrichError(error, activity);
+
+ public virtual void EnrichValidateDocument(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Validate Document";
+
+ if (_options.RenameRootActivity)
+ {
+ UpdateRootActivityName(activity, $"Begin {activity.DisplayName}");
+ }
+
+ var documentInfo = context.OperationDocumentInfo;
+ activity.SetTag("graphql.document.id", documentInfo.Id.Value);
+ activity.SetTag("graphql.document.hash", documentInfo.Hash.Value);
+ }
+
+ public virtual void EnrichValidationError(
+ RequestContext context,
+ Activity activity,
+ IError error)
+ => EnrichError(error, activity);
+
+ public virtual void EnrichAnalyzeOperationComplexity(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Analyze Operation Complexity";
+ }
+
+ public virtual void EnrichCoerceVariables(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Coerce Variable";
+ }
+
+ public virtual void EnrichPlanOperationScope(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Plan Operation";
+ }
+
+ public virtual void EnrichExecuteOperation(RequestContext context, Activity activity)
+ {
+ var plan = context.GetOperationPlan();
+ activity.DisplayName =
+ plan?.OperationName is { } op
+ ? $"Execute Operation {op}"
+ : "Execute Operation";
+ }
+
+ public virtual void EnrichExecuteOperationNode(
+ OperationPlanContext context,
+ OperationExecutionNode node,
+ string schemaName,
+ Activity activity)
+ {
+ activity.DisplayName = $"Execute Operation Node ({schemaName})";
+ activity.SetTag("graphql.fusion.node.id", node.Id);
+ activity.SetTag("graphql.fusion.node.type", node.Type.ToString());
+ activity.SetTag("graphql.fusion.node.schema", schemaName);
+ }
+
+ public virtual void EnrichExecuteOperationBatchNode(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ Activity activity)
+ {
+ activity.DisplayName = $"Execute Operation Batch Node ({schemaName})";
+ activity.SetTag("graphql.fusion.node.id", node.Id);
+ activity.SetTag("graphql.fusion.node.type", node.Type.ToString());
+ activity.SetTag("graphql.fusion.node.schema", schemaName);
+ }
+
+ public virtual void EnrichExecuteNodeFieldNode(
+ OperationPlanContext context,
+ NodeFieldExecutionNode node,
+ Activity activity)
+ {
+ activity.DisplayName = "Execute Node Field Node";
+ activity.SetTag("graphql.fusion.node.id", node.Id);
+ activity.SetTag("graphql.fusion.node.type", node.Type.ToString());
+ }
+
+ public virtual void EnrichExecuteIntrospectionNode(
+ OperationPlanContext context,
+ IntrospectionExecutionNode node,
+ Activity activity)
+ {
+ activity.DisplayName = "Execute Introspection Node";
+ activity.SetTag("graphql.fusion.node.id", node.Id);
+ activity.SetTag("graphql.fusion.node.type", node.Type.ToString());
+ }
+
+ public virtual void EnrichExecutionNodeError(
+ OperationPlanContext context,
+ ExecutionNode node,
+ Exception error,
+ Activity activity)
+ => activity.RecordException(error);
+
+ public virtual void EnrichSourceSchemaError(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ Exception error,
+ Activity activity)
+ => activity.RecordException(error);
+
+ protected virtual void EnrichError(IError error, Activity activity)
+ {
+ if (error.Exception is { } exception)
+ {
+ activity.RecordException(exception);
+ }
+
+ var tags = new ActivityTagsCollection
+ {
+ new(AttributeExceptionMessage, error.Message),
+ new(AttributeExceptionType, error.Code ?? "GRAPHQL_ERROR")
+ };
+
+ if (error.Path is not null)
+ {
+ tags["graphql.error.path"] = error.Path.ToString();
+ }
+
+ if (error.Locations is { Count: > 0 })
+ {
+ tags["graphql.error.location.column"] = error.Locations[0].Column;
+ tags["graphql.error.location.line"] = error.Locations[0].Line;
+ }
+
+ activity.AddEvent(new ActivityEvent(AttributeExceptionEventName, default, tags));
+ }
+}
+
+file static class SemanticConventions
+{
+ public const string AttributeExceptionEventName = "exception";
+ public const string AttributeExceptionType = "exception.type";
+ public const string AttributeExceptionMessage = "exception.message";
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityScopes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityScopes.cs
new file mode 100644
index 00000000000..f0ef6bd29e2
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/FusionActivityScopes.cs
@@ -0,0 +1,37 @@
+namespace HotChocolate.Fusion.Diagnostics;
+
+[Flags]
+public enum FusionActivityScopes
+{
+ None = 0,
+ ExecuteHttpRequest = 1,
+ ParseHttpRequest = 2,
+ FormatHttpResponse = 4,
+ ExecuteRequest = 8,
+ ParseDocument = 16,
+ ValidateDocument = 32,
+ AnalyzeComplexity = 64,
+ CoerceVariables = 128,
+ PlanOperation = 256,
+ ExecuteOperation = 512,
+ ExecuteNodes = 1024,
+ Default =
+ ExecuteHttpRequest
+ | ParseHttpRequest
+ | ValidateDocument
+ | PlanOperation
+ | ExecuteNodes
+ | FormatHttpResponse,
+ All =
+ ExecuteHttpRequest
+ | ParseHttpRequest
+ | FormatHttpResponse
+ | ExecuteRequest
+ | ParseDocument
+ | ValidateDocument
+ | AnalyzeComplexity
+ | CoerceVariables
+ | PlanOperation
+ | ExecuteOperation
+ | ExecuteNodes
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj
new file mode 100644
index 00000000000..0d84beeb354
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj
@@ -0,0 +1,23 @@
+
+
+
+ HotChocolate.Fusion.Diagnostics
+ HotChocolate.Fusion.Diagnostics
+ HotChocolate.Fusion.Diagnostics
+ Provides Hot Chocolate Fusion Diagnostics.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolateFusionActivitySource.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolateFusionActivitySource.cs
new file mode 100644
index 00000000000..dda5993f72b
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/HotChocolateFusionActivitySource.cs
@@ -0,0 +1,15 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Diagnostics.Listeners;
+
+namespace HotChocolate.Fusion.Diagnostics;
+
+internal static class HotChocolateFusionActivitySource
+{
+ public static ActivitySource Source { get; } = new(GetName(), GetVersion());
+
+ public static string GetName()
+ => typeof(ActivityFusionExecutionDiagnosticEventListener).Assembly.GetName().Name!;
+
+ private static string GetVersion()
+ => typeof(ActivityFusionExecutionDiagnosticEventListener).Assembly.GetName().Version!.ToString();
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/InstrumentationOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/InstrumentationOptions.cs
new file mode 100644
index 00000000000..74ae97005eb
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/InstrumentationOptions.cs
@@ -0,0 +1,51 @@
+using static HotChocolate.Fusion.Diagnostics.FusionActivityScopes;
+
+namespace HotChocolate.Fusion.Diagnostics;
+
+///
+/// The Hot Chocolate Fusion instrumentation options.
+///
+public sealed class InstrumentationOptions
+{
+ ///
+ /// Specifies the request detail that shall be included into the tracing activities.
+ ///
+ public RequestDetails RequestDetails { get; set; } = RequestDetails.Default;
+
+ ///
+ /// Specifies the activity scopes that shall be instrumented.
+ ///
+ public FusionActivityScopes Scopes { get; set; } = Default;
+
+ ///
+ /// Specifies if the parsed document shall be included into the tracing data.
+ ///
+ public bool IncludeDocument { get; set; }
+
+ ///
+ /// Defines if the operation display name shall be included in the root activity.
+ ///
+ public bool RenameRootActivity { get; set; }
+
+ internal bool IncludeRequestDetails => RequestDetails is not RequestDetails.None;
+
+ internal bool SkipExecuteHttpRequest => (Scopes & ExecuteHttpRequest) != ExecuteHttpRequest;
+
+ internal bool SkipParseHttpRequest => (Scopes & ParseHttpRequest) != ParseHttpRequest;
+
+ internal bool SkipFormatHttpResponse => (Scopes & FormatHttpResponse) != FormatHttpResponse;
+
+ internal bool SkipExecuteRequest => (Scopes & ExecuteRequest) != ExecuteRequest;
+
+ internal bool SkipParseDocument => (Scopes & ParseDocument) != ParseDocument;
+
+ internal bool SkipValidateDocument => (Scopes & ValidateDocument) != ValidateDocument;
+
+ internal bool SkipCoerceVariables => (Scopes & CoerceVariables) != CoerceVariables;
+
+ internal bool SkipPlanOperation => (Scopes & PlanOperation) != PlanOperation;
+
+ internal bool SkipExecuteOperation => (Scopes & ExecuteOperation) != ExecuteOperation;
+
+ internal bool SkipExecuteNodes => (Scopes & ExecuteNodes) != ExecuteNodes;
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs
new file mode 100644
index 00000000000..985b74a2334
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs
@@ -0,0 +1,358 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Diagnostics.Scopes;
+using HotChocolate.Execution;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using Microsoft.AspNetCore.Http;
+using OpenTelemetry.Trace;
+using static HotChocolate.Fusion.Diagnostics.ContextKeys;
+using static HotChocolate.Fusion.Diagnostics.HotChocolateFusionActivitySource;
+
+namespace HotChocolate.Fusion.Diagnostics.Listeners;
+
+internal sealed class ActivityFusionExecutionDiagnosticEventListener : FusionExecutionDiagnosticEventListener
+{
+ private readonly InstrumentationOptions _options;
+ private readonly FusionActivityEnricher _enricher;
+
+ public ActivityFusionExecutionDiagnosticEventListener(
+ FusionActivityEnricher enricher,
+ InstrumentationOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(enricher);
+ ArgumentNullException.ThrowIfNull(options);
+
+ _enricher = enricher;
+ _options = options;
+ }
+
+ public override IDisposable ExecuteRequest(RequestContext context)
+ {
+ Activity? activity = null;
+
+ if (_options.SkipExecuteRequest)
+ {
+ if (!_options.SkipExecuteHttpRequest
+ && context.ContextData.TryGetValue(nameof(HttpContext), out var value)
+ && value is HttpContext httpContext
+ && httpContext.Items.TryGetValue(HttpRequestActivity, out value)
+ && value is not null)
+ {
+ activity = (Activity)value;
+ }
+ else
+ {
+ return EmptyScope;
+ }
+ }
+
+ activity ??= Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ context.ContextData[RequestActivity] = activity;
+
+ return new ExecuteRequestScope(_enricher, context, activity);
+ }
+
+ public override void RetrievedDocumentFromCache(RequestContext context)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var activity))
+ {
+ Debug.Assert(activity is not null, "The activity mustn't be null!");
+ ((Activity)activity).AddEvent(new(nameof(RetrievedDocumentFromCache)));
+ }
+ }
+
+ public override void RetrievedDocumentFromStorage(RequestContext context)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var activity))
+ {
+ Debug.Assert(activity is not null, "The activity mustn't be null!");
+ ((Activity)activity).AddEvent(new(nameof(RetrievedDocumentFromStorage)));
+ }
+ }
+
+ public override void AddedDocumentToCache(RequestContext context)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var activity))
+ {
+ Debug.Assert(activity is not null, "The activity mustn't be null!");
+ ((Activity)activity).AddEvent(new(nameof(AddedDocumentToCache)));
+ }
+ }
+
+ public override void AddedOperationPlanToCache(RequestContext context, string operationPlanId)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var activity))
+ {
+ Debug.Assert(activity is not null, "The activity mustn't be null!");
+ ((Activity)activity).AddEvent(new(nameof(AddedOperationPlanToCache)));
+ }
+ }
+
+ public override IDisposable ParseDocument(RequestContext context)
+ {
+ if (_options.SkipParseDocument)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ context.ContextData[RequestActivity] = activity;
+
+ return new ParseDocumentScope(_enricher, context, activity);
+ }
+
+ public override void RequestError(RequestContext context, Exception error)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var value))
+ {
+ Debug.Assert(value is not null, "The activity mustn't be null!");
+
+ var activity = (Activity)value;
+ _enricher.EnrichRequestError(context, activity, error);
+ activity.SetStatus(Status.Error);
+ activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+
+ public override void RequestError(RequestContext context, IError error)
+ {
+ if (context.ContextData.TryGetValue(RequestActivity, out var value))
+ {
+ Debug.Assert(value is not null, "The activity mustn't be null!");
+
+ var activity = (Activity)value;
+ _enricher.EnrichRequestError(context, activity, error);
+ activity.SetStatus(Status.Error);
+ activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+
+ public override void ValidationErrors(RequestContext context, IReadOnlyList errors)
+ {
+ if (context.ContextData.TryGetValue(ValidateActivity, out var value))
+ {
+ Debug.Assert(value is not null, "The activity mustn't be null!");
+
+ var activity = (Activity)value;
+
+ foreach (var error in errors)
+ {
+ _enricher.EnrichValidationError(context, activity, error);
+ }
+
+ activity.SetStatus(Status.Error);
+ activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+
+ public override IDisposable ValidateDocument(RequestContext context)
+ {
+ if (_options.SkipValidateDocument)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ context.ContextData[ValidateActivity] = activity;
+
+ return new ValidateDocumentScope(_enricher, context, activity);
+ }
+
+ public override IDisposable CoerceVariables(RequestContext context)
+ {
+ if (_options.SkipCoerceVariables)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new CoerceVariablesScope(_enricher, context, activity);
+ }
+
+ public override IDisposable PlanOperation(RequestContext context, string operationPlanId)
+ {
+ if (_options.SkipPlanOperation)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new PlanOperationScope(_enricher, context, activity);
+ }
+
+ public override IDisposable ExecuteOperation(RequestContext context)
+ {
+ if (_options.SkipExecuteOperation)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new ExecuteOperationScope(_enricher, context, activity);
+ }
+
+ public override IDisposable ExecuteOperationNode(
+ OperationPlanContext context,
+ OperationExecutionNode node,
+ string schemaName)
+ {
+ if (_options.SkipExecuteNodes)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new ExecuteOperationNodeScope(_enricher, context, node, schemaName, activity);
+ }
+
+ public override IDisposable ExecuteOperationBatchNode(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName)
+ {
+ if (_options.SkipExecuteNodes)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new ExecuteOperationBatchNodeScope(_enricher, context, node, schemaName, activity);
+ }
+
+ public override IDisposable ExecuteNodeFieldNode(
+ OperationPlanContext context,
+ NodeFieldExecutionNode node)
+ {
+ if (_options.SkipExecuteNodes)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new ExecuteNodeFieldNodeScope(_enricher, context, node, activity);
+ }
+
+ public override IDisposable ExecuteIntrospectionNode(
+ OperationPlanContext context,
+ IntrospectionExecutionNode node)
+ {
+ if (_options.SkipExecuteNodes)
+ {
+ return EmptyScope;
+ }
+
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return new ExecuteIntrospectionNodeScope(_enricher, context, node, activity);
+ }
+
+ public override void ExecutionNodeError(
+ OperationPlanContext context,
+ ExecutionNode node,
+ Exception error)
+ {
+ if (Activity.Current is { } activity)
+ {
+ _enricher.EnrichExecutionNodeError(context, node, error, activity);
+ }
+ }
+
+ public override void SourceSchemaTransportError(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ Exception error)
+ {
+ if (Activity.Current is { } activity)
+ {
+ _enricher.EnrichSourceSchemaError(context, node, schemaName, error, activity);
+ }
+ }
+
+ public override void SourceSchemaStoreError(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ Exception error)
+ {
+ if (Activity.Current is { } activity)
+ {
+ _enricher.EnrichSourceSchemaError(context, node, schemaName, error, activity);
+ }
+ }
+
+ public override IDisposable OnSubscriptionEvent(
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ ulong subscriptionId)
+ {
+ var activity = Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ return activity;
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs
new file mode 100644
index 00000000000..1eb078b2f3f
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs
@@ -0,0 +1,152 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using HotChocolate.AspNetCore.Instrumentation;
+using HotChocolate.Execution;
+using HotChocolate.Language;
+using OpenTelemetry.Trace;
+using static HotChocolate.Fusion.Diagnostics.ContextKeys;
+
+namespace HotChocolate.Fusion.Diagnostics.Listeners;
+
+internal sealed class ActivityServerDiagnosticListener(
+ FusionActivityEnricher enricher,
+ InstrumentationOptions options)
+ : ServerDiagnosticEventListener
+{
+ private readonly InstrumentationOptions _options = options ?? throw new ArgumentNullException(nameof(options));
+ private readonly FusionActivityEnricher _enricher = enricher ?? throw new ArgumentNullException(nameof(enricher));
+
+ public override IDisposable ExecuteHttpRequest(HttpContext context, HttpRequestKind kind)
+ {
+ if (_options.SkipExecuteHttpRequest)
+ {
+ return EmptyScope;
+ }
+
+ var activity = HotChocolateFusionActivitySource.Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ _enricher.EnrichExecuteHttpRequest(context, kind, activity);
+ activity.SetStatus(ActivityStatusCode.Ok);
+ context.Items[HttpRequestActivity] = activity;
+
+ return activity;
+ }
+
+ public override void StartSingleRequest(HttpContext context, GraphQLRequest request)
+ {
+ if (_options.IncludeRequestDetails
+ && context.Items.TryGetValue(HttpRequestActivity, out var activity))
+ {
+ _enricher.EnrichSingleRequest(context, request, (Activity)activity!);
+ }
+ }
+
+ public override void StartBatchRequest(HttpContext context, IReadOnlyList batch)
+ {
+ if (_options.IncludeRequestDetails
+ && context.Items.TryGetValue(HttpRequestActivity, out var activity))
+ {
+ _enricher.EnrichBatchRequest(context, batch, (Activity)activity!);
+ }
+ }
+
+ public override void StartOperationBatchRequest(
+ HttpContext context,
+ GraphQLRequest request,
+ IReadOnlyList operations)
+ {
+ if (_options.IncludeRequestDetails
+ && context.Items.TryGetValue(HttpRequestActivity, out var activity))
+ {
+ _enricher.EnrichOperationBatchRequest(
+ context,
+ request,
+ operations,
+ (Activity)activity!);
+ }
+ }
+
+ public override void HttpRequestError(HttpContext context, IError error)
+ {
+ if (context.Items.TryGetValue(HttpRequestActivity, out var value))
+ {
+ var activity = (Activity)value!;
+ _enricher.EnrichHttpRequestError(context, error, activity);
+ activity.SetStatus(Status.Error);
+ }
+ }
+
+ public override void HttpRequestError(HttpContext context, Exception exception)
+ {
+ if (context.Items.TryGetValue(HttpRequestActivity, out var value))
+ {
+ var activity = (Activity)value!;
+ _enricher.EnrichHttpRequestError(context, exception, activity);
+ activity.SetStatus(Status.Error);
+ }
+ }
+
+ public override IDisposable ParseHttpRequest(HttpContext context)
+ {
+ if (_options.SkipParseHttpRequest)
+ {
+ return EmptyScope;
+ }
+
+ var activity = HotChocolateFusionActivitySource.Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ _enricher.EnrichParseHttpRequest(context, activity);
+ activity.SetStatus(Status.Ok);
+ activity.SetStatus(ActivityStatusCode.Ok);
+ context.Items[ParseHttpRequestActivity] = activity;
+
+ return activity;
+ }
+
+ public override void ParserErrors(HttpContext context, IReadOnlyList errors)
+ {
+ if (context.Items.TryGetValue(ParseHttpRequestActivity, out var value))
+ {
+ var activity = (Activity)value!;
+
+ foreach (var error in errors)
+ {
+ _enricher.EnrichParserErrors(context, error, activity);
+ }
+
+ activity.SetStatus(Status.Error);
+ activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+
+ public override IDisposable FormatHttpResponse(HttpContext context, OperationResult result)
+ {
+ if (_options.SkipFormatHttpResponse)
+ {
+ return EmptyScope;
+ }
+
+ var activity = HotChocolateFusionActivitySource.Source.StartActivity();
+
+ if (activity is null)
+ {
+ return EmptyScope;
+ }
+
+ _enricher.EnrichFormatHttpResponse(context, activity);
+ activity.SetStatus(ActivityStatusCode.Ok);
+ context.Items[FormatHttpResponseActivity] = activity;
+
+ return activity;
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/RequestDetails.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/RequestDetails.cs
new file mode 100644
index 00000000000..dd05a951ba2
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/RequestDetails.cs
@@ -0,0 +1,15 @@
+namespace HotChocolate.Fusion.Diagnostics;
+
+[Flags]
+public enum RequestDetails
+{
+ None = 0,
+ Id = 1,
+ Hash = 2,
+ Operation = 4,
+ Variables = 8,
+ Extensions = 16,
+ Query = 32,
+ Default = Id | Hash | Operation | Extensions,
+ All = Id | Hash | Operation | Variables | Extensions | Query
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs
new file mode 100644
index 00000000000..8361d9d044d
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class CoerceVariablesScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichCoerceVariables(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.VariableValues.Length > 0)
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteIntrospectionNodeScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteIntrospectionNodeScope.cs
new file mode 100644
index 00000000000..622397a1e7d
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteIntrospectionNodeScope.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteIntrospectionNodeScope(
+ FusionActivityEnricher enricher,
+ OperationPlanContext context,
+ IntrospectionExecutionNode node,
+ Activity activity)
+ : NodeScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteIntrospectionNode(Context, node, Activity);
+
+ protected override void SetStatus()
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteNodeFieldNodeScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteNodeFieldNodeScope.cs
new file mode 100644
index 00000000000..867b33786e5
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteNodeFieldNodeScope.cs
@@ -0,0 +1,23 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteNodeFieldNodeScope(
+ FusionActivityEnricher enricher,
+ OperationPlanContext context,
+ NodeFieldExecutionNode node,
+ Activity activity)
+ : NodeScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteNodeFieldNode(Context, node, Activity);
+
+ protected override void SetStatus()
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationBatchNodeScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationBatchNodeScope.cs
new file mode 100644
index 00000000000..1363edffea5
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationBatchNodeScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteOperationBatchNodeScope(
+ FusionActivityEnricher enricher,
+ OperationPlanContext context,
+ ExecutionNode node,
+ string schemaName,
+ Activity activity)
+ : NodeScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteOperationBatchNode(Context, node, schemaName, Activity);
+
+ protected override void SetStatus()
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationNodeScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationNodeScope.cs
new file mode 100644
index 00000000000..58e19e11185
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationNodeScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Execution;
+using HotChocolate.Fusion.Execution.Nodes;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteOperationNodeScope(
+ FusionActivityEnricher enricher,
+ OperationPlanContext context,
+ OperationExecutionNode node,
+ string schemaName,
+ Activity activity)
+ : NodeScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteOperationNode(Context, node, schemaName, Activity);
+
+ protected override void SetStatus()
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs
new file mode 100644
index 00000000000..1f69711d4e6
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs
@@ -0,0 +1,29 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteOperationScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteOperation(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.Result is null or OperationResult { Errors: [_, ..] })
+ {
+ Activity.SetStatus(Status.Error);
+ Activity.SetStatus(ActivityStatusCode.Error);
+ }
+ else
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs
new file mode 100644
index 00000000000..6ecca09cc07
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteRequestScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteRequest(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.Result is null or OperationResult { Errors: [_, ..] })
+ {
+ Activity.SetStatus(Status.Error);
+ Activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/NodeScopeBase.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/NodeScopeBase.cs
new file mode 100644
index 00000000000..e0c04291939
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/NodeScopeBase.cs
@@ -0,0 +1,40 @@
+using System.Diagnostics;
+using HotChocolate.Fusion.Execution;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal class NodeScopeBase : IDisposable
+{
+ private bool _disposed;
+
+ protected NodeScopeBase(
+ FusionActivityEnricher enricher,
+ OperationPlanContext context,
+ Activity activity)
+ {
+ Enricher = enricher ?? throw new ArgumentNullException(nameof(enricher));
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Activity = activity ?? throw new ArgumentNullException(nameof(activity));
+ }
+
+ protected FusionActivityEnricher Enricher { get; }
+
+ protected OperationPlanContext Context { get; }
+
+ protected Activity Activity { get; }
+
+ protected virtual void EnrichActivity() { }
+
+ protected virtual void SetStatus() { }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ EnrichActivity();
+ SetStatus();
+ Activity.Dispose();
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs
new file mode 100644
index 00000000000..048af8072b1
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ParseDocumentScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichParseDocument(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.TryGetOperationDocument(out _, out _))
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs
new file mode 100644
index 00000000000..aad6df000df
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class PlanOperationScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichPlanOperationScope(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.GetOperationPlan() is not null)
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/RequestScopeBase.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/RequestScopeBase.cs
new file mode 100644
index 00000000000..69403a62676
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/RequestScopeBase.cs
@@ -0,0 +1,40 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal class RequestScopeBase : IDisposable
+{
+ private bool _disposed;
+
+ protected RequestScopeBase(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ {
+ Enricher = enricher ?? throw new ArgumentNullException(nameof(enricher));
+ Context = context ?? throw new ArgumentNullException(nameof(context));
+ Activity = activity ?? throw new ArgumentNullException(nameof(activity));
+ }
+
+ protected FusionActivityEnricher Enricher { get; }
+
+ protected RequestContext Context { get; }
+
+ protected Activity Activity { get; }
+
+ protected virtual void EnrichActivity() { }
+
+ protected virtual void SetStatus() { }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ EnrichActivity();
+ SetStatus();
+ Activity.Dispose();
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs
new file mode 100644
index 00000000000..2f4b9dc58cf
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ValidateDocumentScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : RequestScopeBase(enricher, context, activity)
+{
+ protected override void EnrichActivity()
+ => Enricher.EnrichValidateDocument(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.IsOperationDocumentValid())
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs
index a09cde1ead2..847f14a71ce 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/DependencyInjection/HotChocolateFusionServiceCollectionExtensions.cs
@@ -23,8 +23,7 @@ public static IFusionGatewayBuilder AddGraphQLGateway(
ArgumentNullException.ThrowIfNull(services);
ArgumentException.ThrowIfNullOrEmpty(name);
- services.AddOptions();
-
+ AddCore(services);
AddRequestExecutorManager(services);
AddSourceSchemaScope(services);
AddResultObjectPools(services, options.Clone());
@@ -32,6 +31,20 @@ public static IFusionGatewayBuilder AddGraphQLGateway(
return CreateBuilder(services, name);
}
+ private static void AddCore(
+ IServiceCollection services)
+ {
+ services.AddOptions();
+
+ services.TryAddSingleton();
+ services.TryAddSingleton(sp =>
+ {
+ var provider = sp.GetRequiredService();
+ var policy = new StringBuilderPooledObjectPolicy();
+ return provider.Create(policy);
+ });
+ }
+
private static void AddRequestExecutorManager(
IServiceCollection services)
{
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs
index f13aee78f07..2f81373196f 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs
@@ -223,18 +223,6 @@ public void SourceSchemaStoreError(
}
}
- public void SourceSchemaResultError(
- OperationPlanContext context,
- ExecutionNode node,
- string schemaName,
- IReadOnlyList errors)
- {
- for (var i = 0; i < listeners.Length; i++)
- {
- listeners[i].SourceSchemaResultError(context, node, schemaName, errors);
- }
- }
-
public void ExecutionNodeError(OperationPlanContext context, ExecutionNode node, Exception error)
{
for (var i = 0; i < listeners.Length; i++)
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs
index f0cfc6e9bb6..f54cd83f3e2 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs
@@ -118,14 +118,6 @@ public virtual void SourceSchemaStoreError(
{
}
- ///
- public virtual void SourceSchemaResultError(
- OperationPlanContext context,
- ExecutionNode node, string schemaName,
- IReadOnlyList errors)
- {
- }
-
///
public virtual void ExecutionNodeError(
OperationPlanContext context,
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs
index e27c5c5e2b0..56e50633fc0 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs
@@ -234,29 +234,6 @@ void SourceSchemaStoreError(
string schemaName,
Exception error);
- ///
- /// Called when GraphQL errors are present in the source schema result.
- /// These are application-level errors returned by the source schema,
- /// not transport or communication errors.
- ///
- ///
- /// The operation plan context.
- ///
- ///
- /// The execution node that received the erroneous response.
- ///
- ///
- /// The name of the source schema that returned the errors.
- ///
- ///
- /// The collection of GraphQL errors from the source schema response.
- ///
- void SourceSchemaResultError(
- OperationPlanContext context,
- ExecutionNode node,
- string schemaName,
- IReadOnlyList errors);
-
///
/// Called when a transport error occurs while communicating with a source schema
/// during subscription operations. This includes connection drops, network timeouts,
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs
index 16fd60604fa..96eecbf0826 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs
@@ -442,6 +442,11 @@ private void AddCoreServices(
static _ => new DefaultObjectPool(
new RequestContextPooledObjectPolicy()));
+ services.TryAddSingleton(
+ static _ => new DefaultObjectPoolProvider());
+ services.AddSingleton(
+ static sp => sp.GetRequiredService().CreateStringBuilderPool());
+
services.AddTransient(static _ => new IntrospectionFieldInterceptor());
}
@@ -449,9 +454,6 @@ private static void AddOperationPlanner(
IServiceCollection services,
OperationPlannerOptions plannerOptions)
{
- services.TryAddSingleton(
- static _ => new DefaultObjectPoolProvider());
-
services.AddSingleton(
static sp => sp.GetRequiredService().CreateFieldMapPool());
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs
new file mode 100644
index 00000000000..af0a8d63288
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs
@@ -0,0 +1,131 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using HotChocolate.Utilities;
+
+namespace HotChocolate.Fusion.Diagnostics;
+
+public static partial class ActivityTestHelper
+{
+ public static IDisposable CaptureActivities(out object activities)
+ {
+ var sync = new object();
+ var listener = new ActivityListener();
+ var root = new OrderedDictionary();
+ var lookup = new Dictionary>();
+ Activity rootActivity = null!;
+
+ listener.ShouldListenTo = source =>
+ string.Equals(source.Name, "HotChocolate.Fusion.Diagnostics", StringComparison.Ordinal);
+ listener.ActivityStarted = a =>
+ {
+ lock (sync)
+ {
+ if (a.Parent is null
+ && string.Equals(a.OperationName, "ExecuteHttpRequest", StringComparison.Ordinal)
+ && lookup.TryGetValue(rootActivity, out var parentData))
+ {
+ RegisterActivity(a, parentData);
+ lookup[a] = (OrderedDictionary)a.GetCustomProperty("test.data")!;
+ }
+
+ if (a.Parent is not null
+ && lookup.TryGetValue(a.Parent, out parentData))
+ {
+ RegisterActivity(a, parentData);
+ lookup[a] = (OrderedDictionary)a.GetCustomProperty("test.data")!;
+ }
+ }
+ };
+ listener.ActivityStopped = SerializeActivity;
+ listener.Sample = (ref ActivityCreationOptions _) =>
+ ActivitySamplingResult.AllData;
+ ActivitySource.AddActivityListener(listener);
+
+ rootActivity = HotChocolateFusionActivitySource.Source.StartActivity()!;
+ rootActivity.SetCustomProperty("test.data", root);
+ lookup[rootActivity] = root;
+
+ activities = root;
+ return new Session(rootActivity, listener);
+ }
+
+ private static void RegisterActivity(
+ Activity activity,
+ OrderedDictionary parent)
+ {
+ if (!(parent.TryGetValue("activities", out var value) && value is List