diff --git a/src/All.slnx b/src/All.slnx
index 58ab5c044d7..d7a3a9b9f1d 100644
--- a/src/All.slnx
+++ b/src/All.slnx
@@ -177,9 +177,11 @@
+
+
diff --git a/src/HotChocolate/Diagnostics/HotChocolate.Diagnostics.slnx b/src/HotChocolate/Diagnostics/HotChocolate.Diagnostics.slnx
index 3c4f93bf3f9..d95977030e8 100644
--- a/src/HotChocolate/Diagnostics/HotChocolate.Diagnostics.slnx
+++ b/src/HotChocolate/Diagnostics/HotChocolate.Diagnostics.slnx
@@ -1,8 +1,10 @@
+
+
diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
index 0357532fffc..531406760fa 100644
--- a/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
+++ b/src/HotChocolate/Diagnostics/src/Diagnostics/ActivityEnricher.cs
@@ -31,13 +31,13 @@ public class ActivityEnricher
///
/// Initializes a new instance of .
///
- ///
+ ///
///
protected ActivityEnricher(
- ObjectPool stringBuilderPoolPool,
+ ObjectPool stringBuilderPool,
InstrumentationOptions options)
{
- StringBuilderPool = stringBuilderPoolPool;
+ StringBuilderPool = stringBuilderPool;
_options = options;
}
diff --git a/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
index 0040e709956..d05a66a6406 100644
--- a/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Diagnostics/src/Diagnostics/Extensions/DiagnosticsRequestExecutorBuilderExtensions.cs
@@ -82,9 +82,9 @@ public static IRequestExecutorBuilder AddInstrumentation(
private sealed class InternalActivityEnricher : ActivityEnricher
{
public InternalActivityEnricher(
- ObjectPool stringBuilderPoolPool,
+ ObjectPool stringBuilderPool,
InstrumentationOptions options)
- : base(stringBuilderPoolPool, options)
+ : base(stringBuilderPool, options)
{
}
}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/ContextKeys.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/ContextKeys.cs
new file mode 100644
index 00000000000..f35bd4946f0
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs
new file mode 100644
index 00000000000..f90bec222c3
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Extensions/DiagnosticsFusionGatewayBuilderExtensions.cs
@@ -0,0 +1,58 @@
+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.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 : FusionActivityEnricher
+ {
+ public InternalActivityEnricher(
+ ObjectPool stringBuilderPool,
+ InstrumentationOptions options)
+ : base(stringBuilderPool, options)
+ {
+ }
+ }
+}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Extensions/TracerProviderBuilderExtensions.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Extensions/TracerProviderBuilderExtensions.cs
new file mode 100644
index 00000000000..e02cee949d2
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/FusionActivityEnricher.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/FusionActivityEnricher.cs
new file mode 100644
index 00000000000..64be8d71bb5
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/FusionActivityEnricher.cs
@@ -0,0 +1,587 @@
+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.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)
+ {
+ var node = CreateVariablesNode(request.Variables);
+ EnrichRequestVariables(context, request, node, 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)
+ {
+ var node = CreateVariablesNode(request.Variables);
+ EnrichBatchVariables(context, request, node, 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)
+ {
+ var node = CreateVariablesNode(request.Variables);
+ EnrichRequestVariables(context, request, node, 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,
+ ISyntaxNode variables,
+ Activity activity)
+ {
+ activity.SetTag("graphql.http.request.variables", variables.Print());
+ }
+
+ protected virtual void EnrichBatchVariables(
+ HttpContext context,
+ GraphQLRequest request,
+ ISyntaxNode variables,
+ int index,
+ Activity activity)
+ {
+ activity.SetTag($"graphql.http.request[{index}].variables", variables.Print());
+ }
+
+ protected virtual void EnrichRequestExtensions(
+ HttpContext context,
+ GraphQLRequest request,
+ IReadOnlyDictionary extensions,
+ Activity activity)
+ {
+ try
+ {
+ activity.SetTag(
+ "graphql.http.request.extensions",
+ JsonSerializer.Serialize(extensions));
+ }
+ catch
+ {
+ // Ignore any errors
+ }
+ }
+
+ protected virtual void EnrichBatchExtensions(
+ HttpContext context,
+ GraphQLRequest request,
+ IReadOnlyDictionary extensions,
+ int index,
+ Activity activity)
+ {
+ try
+ {
+ activity.SetTag(
+ $"graphql.http.request[{index}].extensions",
+ JsonSerializer.Serialize(extensions));
+ }
+ 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;
+ // TODO: Why do we do this?
+ 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); // TODO: This is wrong
+ 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 IOperationResult result)
+ {
+ var errorCount = result.Errors?.Count ?? 0;
+ activity.SetTag("graphql.errors.count", errorCount);
+ }
+ }
+
+ protected virtual string? CreateOperationDisplayName(RequestContext context, OperationPlan? plan)
+ {
+ if (plan is null)
+ {
+ return null;
+ }
+
+ var displayName = StringBuilderPool.Get();
+
+ try
+ {
+ var rootSelectionSet = plan.Operation.RootSelectionSet;
+
+ displayName.Append('{');
+ displayName.Append(' ');
+
+ foreach (var selection in rootSelectionSet.Selections[..3])
+ {
+ 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 EnrichCompileOperation(RequestContext context, Activity activity)
+ {
+ activity.DisplayName = "Compile Operation";
+ }
+
+ public virtual void EnrichExecuteOperation(RequestContext context, Activity activity)
+ {
+ var plan = context.GetOperationPlan();
+ activity.DisplayName =
+ plan?.OperationName is { } op
+ ? $"Execute Operation {op}"
+ : "Execute Operation";
+ }
+
+ 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));
+ }
+
+ private static ISyntaxNode CreateVariablesNode(
+ IReadOnlyList>? variableSet)
+ {
+ if (variableSet is null or { Count: 0 })
+ {
+ return NullValueNode.Default;
+ }
+
+ if (variableSet.Count == 1)
+ {
+ var variables = variableSet[0];
+ var variablesCount = variables.Count;
+ var fields = new ObjectFieldNode[variablesCount];
+ var index = 0;
+
+ foreach (var (name, value) in variables)
+ {
+ // since we are in the HTTP context here we know that it will always be an IValueNode.
+ var valueNode = value is null ? NullValueNode.Default : (IValueNode)value;
+ fields[index++] = new ObjectFieldNode(name, valueNode);
+ }
+
+ return new ObjectValueNode(fields);
+ }
+
+ if (variableSet.Count > 0)
+ {
+ var variableSetCount = variableSet.Count;
+ var items = new IValueNode[variableSetCount];
+
+ for (var i = 0; i < variableSetCount; i++)
+ {
+ var variables = variableSet[i];
+ var variablesCount = variables.Count;
+ var fields = new ObjectFieldNode[variablesCount];
+ var index = 0;
+
+ foreach (var (name, value) in variables)
+ {
+ // since we are in the HTTP context here we know that it will always be an IValueNode.
+ var valueNode = value is null ? NullValueNode.Default : (IValueNode)value;
+ fields[index++] = new ObjectFieldNode(name, valueNode);
+ }
+
+ items[i] = new ObjectValueNode(fields);
+ }
+ }
+
+ throw new InvalidOperationException();
+ }
+}
+
+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/Diagnostics/src/Fusion.Diagnostics/FusionActivityScopes.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/FusionActivityScopes.cs
new file mode 100644
index 00000000000..d38cd4737f7
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/FusionActivityScopes.cs
@@ -0,0 +1,34 @@
+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,
+ Default =
+ ExecuteHttpRequest
+ | ParseHttpRequest
+ | ValidateDocument
+ | PlanOperation
+ | FormatHttpResponse,
+ All =
+ ExecuteHttpRequest
+ | ParseHttpRequest
+ | FormatHttpResponse
+ | ExecuteRequest
+ | ParseDocument
+ | ValidateDocument
+ | AnalyzeComplexity
+ | CoerceVariables
+ | PlanOperation
+ | ExecuteOperation
+}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/HotChocolate.Fusion.Diagnostics.csproj
new file mode 100644
index 00000000000..0d84beeb354
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/HotChocolateFusionActivitySource.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/HotChocolateFusionActivitySource.cs
new file mode 100644
index 00000000000..dda5993f72b
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/InstrumentationOptions.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/InstrumentationOptions.cs
new file mode 100644
index 00000000000..17b3d7ab1bc
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/InstrumentationOptions.cs
@@ -0,0 +1,49 @@
+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;
+}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs
new file mode 100644
index 00000000000..7103a9bd8bc
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityFusionExecutionDiagnosticEventListener.cs
@@ -0,0 +1,247 @@
+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;
+
+// TODO: Add more items
+// TODO: Check if AddedOperationPlanToCache is correct
+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 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/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs
new file mode 100644
index 00000000000..d3135287ae0
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Listeners/ActivityServerDiagnosticListener.cs
@@ -0,0 +1,158 @@
+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;
+
+// TODO: Variable batching events?
+internal sealed class ActivityServerDiagnosticListener : ServerDiagnosticEventListener
+{
+ private readonly InstrumentationOptions _options;
+ private readonly FusionActivityEnricher _enricher;
+
+ public ActivityServerDiagnosticListener(
+ FusionActivityEnricher enricher,
+ InstrumentationOptions options)
+ {
+ _enricher = enricher ?? throw new ArgumentNullException(nameof(enricher));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ 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, IOperationResult 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/Diagnostics/src/Fusion.Diagnostics/RequestDetails.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/RequestDetails.cs
new file mode 100644
index 00000000000..dd05a951ba2
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs
new file mode 100644
index 00000000000..ebee57d1a10
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/CoerceVariablesScope.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class CoerceVariablesScope : RequestScopeBase
+{
+ public CoerceVariablesScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(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/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs
new file mode 100644
index 00000000000..ddca7f05bbd
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteOperationScope.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteOperationScope : RequestScopeBase
+{
+ public ExecuteOperationScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(enricher, context, activity)
+ {
+ }
+
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteOperation(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.Result is null or IOperationResult { Errors: [_, ..] })
+ {
+ Activity.SetStatus(Status.Error);
+ Activity.SetStatus(ActivityStatusCode.Error);
+ }
+ else
+ {
+ Activity.SetStatus(Status.Ok);
+ Activity.SetStatus(ActivityStatusCode.Ok);
+ }
+ }
+}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs
new file mode 100644
index 00000000000..4d3946fee8c
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ExecuteRequestScope.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ExecuteRequestScope : RequestScopeBase
+{
+ public ExecuteRequestScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(enricher, context, activity)
+ {
+ }
+
+ protected override void EnrichActivity()
+ => Enricher.EnrichExecuteRequest(Context, Activity);
+
+ protected override void SetStatus()
+ {
+ if (Context.Result is null or IOperationResult { Errors: [_, ..] })
+ {
+ Activity.SetStatus(Status.Error);
+ Activity.SetStatus(ActivityStatusCode.Error);
+ }
+ }
+}
diff --git a/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs
new file mode 100644
index 00000000000..b795f894f06
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ParseDocumentScope.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ParseDocumentScope : RequestScopeBase
+{
+ public ParseDocumentScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(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/Diagnostics/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs
new file mode 100644
index 00000000000..5a09079ef7e
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/PlanOperationScope.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class PlanOperationScope : RequestScopeBase
+{
+ public PlanOperationScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(enricher, context, activity)
+ {
+ }
+
+ protected override void EnrichActivity()
+ => Enricher.EnrichCompileOperation(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/Diagnostics/src/Fusion.Diagnostics/Scopes/RequestScopeBase.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/RequestScopeBase.cs
new file mode 100644
index 00000000000..69403a62676
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/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/Diagnostics/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs
new file mode 100644
index 00000000000..a4ff4d161b3
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/src/Fusion.Diagnostics/Scopes/ValidateDocumentScope.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics;
+using HotChocolate.Execution;
+using OpenTelemetry.Trace;
+
+namespace HotChocolate.Fusion.Diagnostics.Scopes;
+
+internal sealed class ValidateDocumentScope : RequestScopeBase
+{
+ public ValidateDocumentScope(
+ FusionActivityEnricher enricher,
+ RequestContext context,
+ Activity activity)
+ : base(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/Diagnostics/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs b/src/HotChocolate/Diagnostics/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs
new file mode 100644
index 00000000000..2751d1e7c94
--- /dev/null
+++ b/src/HotChocolate/Diagnostics/test/Fusion.Diagnostics.Tests/ActivityTestHelper.cs
@@ -0,0 +1,100 @@
+using System.Diagnostics;
+using HotChocolate.Utilities;
+
+namespace HotChocolate.Fusion.Diagnostics;
+
+public static 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