diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs index d9e6af3875a..5de61c9b06a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaErrors.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections.Immutable; using System.Text.Json; +using HotChocolate.Fusion.Execution.Nodes; namespace HotChocolate.Fusion.Execution.Clients; @@ -27,6 +28,8 @@ public sealed class SourceSchemaErrors /// /// A representing the "errors" array from a GraphQL response. /// + /// + /// /// /// A instance containing the parsed errors, or /// null if the JSON is not a valid array format. @@ -34,7 +37,7 @@ public sealed class SourceSchemaErrors /// /// Thrown when an error path contains unsupported element types (only strings and integer are supported). /// - public static SourceSchemaErrors? From(JsonElement json) + public static SourceSchemaErrors? From(JsonElement json, OperationPlanContext context, ExecutionNode sourceNode) { if (json.ValueKind != JsonValueKind.Array) { @@ -48,13 +51,20 @@ public sealed class SourceSchemaErrors { var currentTrie = root; - var error = CreateError(jsonError); + var errorBuilder = CreateErrorBuilder(jsonError); - if (error is null) + if (errorBuilder is null) { continue; } + if (context.CollectTelemetry) + { + errorBuilder.SetExtension(WellKnownErrorExtensions.SourceOperationPlanNodeId, sourceNode.Id); + } + + var error = errorBuilder.Build(); + if (error.Path is null) { rootErrors ??= ImmutableArray.CreateBuilder(); @@ -100,7 +110,7 @@ public sealed class SourceSchemaErrors return new SourceSchemaErrors { RootErrors = rootErrors?.ToImmutableArray() ?? [], Trie = root }; } - private static IError? CreateError(JsonElement jsonError) + private static ErrorBuilder? CreateErrorBuilder(JsonElement jsonError) { if (jsonError.ValueKind is not JsonValueKind.Object) { @@ -133,7 +143,7 @@ public sealed class SourceSchemaErrors } } - return errorBuilder.Build(); + return errorBuilder; } return null; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs index 22eaea55611..5d617095d47 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaResult.cs @@ -20,7 +20,7 @@ public SourceSchemaResult( _resource = resource; Path = path; Data = data; - Errors = SourceSchemaErrors.From(errors); + Errors = errors; Extensions = extensions; Final = final; } @@ -29,7 +29,7 @@ public SourceSchemaResult( public JsonElement Data { get; } - public SourceSchemaErrors? Errors { get; } + public JsonElement Errors { get; } public JsonElement Extensions { get; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs index 584b050c445..30c0717a08e 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs @@ -110,7 +110,8 @@ public void CompleteNode(ExecutionNode node, ExecutionNodeResult result) SpanId = result.Activity?.SpanId.ToHexString(), Status = result.Status, Duration = result.Duration, - VariableSets = result.VariableValueSets + VariableSets = result.VariableValueSets, + SchemaName = result.SchemaName }); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs index d2dc921a09b..60e2b3a8209 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs @@ -57,7 +57,8 @@ public async Task ExecuteAsync( Stopwatch.GetElapsedTime(start), error, context.GetDependentsToExecute(this), - context.GetVariableValueSets(this)); + context.GetVariableValueSets(this), + context.GetSchemaName(this)); context.CompleteNode(result); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeResult.cs index f571b2f2f73..624bc28bb06 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeResult.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeResult.cs @@ -10,4 +10,5 @@ internal sealed record ExecutionNodeResult( TimeSpan Duration, Exception? Exception, ImmutableArray DependentsToExecute, - ImmutableArray VariableValueSets); + ImmutableArray VariableValueSets, + string? SchemaName); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeTrace.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeTrace.cs index 3f9087b5f1a..ff3d3bdafea 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeTrace.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeTrace.cs @@ -12,5 +12,7 @@ public sealed class ExecutionNodeTrace public required ExecutionStatus Status { get; init; } - public ImmutableArray VariableSets { get; init; } + public required ImmutableArray VariableSets { get; init; } + + public required string? SchemaName { get; init; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeExecutionNode.cs index 871549545df..04a003c54fe 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeExecutionNode.cs @@ -68,12 +68,16 @@ protected override ValueTask OnExecuteAsync( || !context.TryGetNodeLookupSchemaForType(typeName, out var schemaName)) { // We have an invalid id or a valid id of a type that does not implement the Node interface - var error = ErrorBuilder.New() + var errorBuilder = ErrorBuilder.New() .SetMessage("The node ID string has an invalid format.") - .SetExtension("originalValue", id) - .Build(); + .SetExtension(WellKnownErrorExtensions.OriginalIdValue, id); - context.AddErrors(error, [_responseName], Path.Root); + if (context.CollectTelemetry) + { + errorBuilder.SetExtension(WellKnownErrorExtensions.SourceOperationPlanNodeId, Id); + } + + context.AddErrors(errorBuilder.Build(), [_responseName], Path.Root); return ValueTask.FromResult(ExecutionStatus.Failed); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index b740dfe2e6a..0b35b97a825 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -117,41 +117,49 @@ protected override async ValueTask OnExecuteAsync( } catch (Exception exception) { - AddErrors(context, exception, variables, _responseNames); + AddErrors(context, exception, variables); return ExecutionStatus.Failed; } var index = 0; var bufferLength = Math.Max(variables.Length, 1); - var buffer = ArrayPool.Shared.Rent(bufferLength); + var resultBuffer = ArrayPool.Shared.Rent(bufferLength); + var errorBuffer = ArrayPool.Shared.Rent(bufferLength); try { await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) { - buffer[index++] = result; + resultBuffer[index] = result; + errorBuffer[index] = SourceSchemaErrors.From(result.Errors, context, this); + + index++; } - context.AddPartialResults(_source, buffer.AsSpan(0, index), _responseNames); + context.AddPartialResults( + _source, + resultBuffer.AsSpan(0, index), + errorBuffer.AsSpan(0, index), + _responseNames); } catch (Exception exception) { // if there is an error, we need to make sure that the pooled buffers for the JsonDocuments // are returned to the pool. - foreach (var result in buffer.AsSpan(0, index)) + foreach (var result in resultBuffer.AsSpan(0, index)) { // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract result?.Dispose(); } - AddErrors(context, exception, variables, _responseNames); + AddErrors(context, exception, variables); return ExecutionStatus.Failed; } finally { - buffer.AsSpan(0, index).Clear(); - ArrayPool.Shared.Return(buffer); + resultBuffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(resultBuffer); } return ExecutionStatus.Success; @@ -194,23 +202,29 @@ internal async Task SubscribeAsync( } catch (Exception exception) { - AddErrors(context, exception, variables, _responseNames); + AddErrors(context, exception, variables); return SubscriptionResult.Failed(); } } - private static void AddErrors( + private void AddErrors( OperationPlanContext context, Exception exception, - ImmutableArray variables, - ReadOnlySpan responseNames) + ImmutableArray variables) { - var error = ErrorBuilder.FromException(exception).Build(); + var errorBuilder = ErrorBuilder.FromException(exception); + + if (context.CollectTelemetry) + { + errorBuilder.SetExtension(WellKnownErrorExtensions.SourceOperationPlanNodeId, Id); + } + + var error = errorBuilder.Build(); if (variables.Length == 0) { - context.AddErrors(error, responseNames, Path.Root); + context.AddErrors(error, _responseNames, Path.Root); } else { @@ -224,7 +238,7 @@ private static void AddErrors( pathBuffer[i] = variables[i].Path; } - context.AddErrors(error, responseNames, pathBuffer.AsSpan(0, pathBufferLength)); + context.AddErrors(error, _responseNames, pathBuffer.AsSpan(0, pathBufferLength)); } finally { @@ -278,6 +292,7 @@ private sealed class SubscriptionEnumerator : IAsyncEnumerator MoveNextAsync() if (hasResult) { - _resultBuffer[0] = _resultEnumerator.Current; - _context.AddPartialResults(_node._source, _resultBuffer, _node._responseNames); + var result = _resultEnumerator.Current; + _resultBuffer[0] = result; + _errorsBuffer[0] = SourceSchemaErrors.From(result.Errors, _context, _node); + _context.AddPartialResults(_node._source, _resultBuffer, _errorsBuffer, _node._responseNames); } } catch (Exception exception) @@ -340,7 +357,14 @@ public async ValueTask MoveNextAsync() Exception: exception, VariableValueSets: _context.GetVariableValueSets(_node)); - var error = ErrorBuilder.FromException(exception).Build(); + var errorBuilder = ErrorBuilder.FromException(exception); + + if (_context.CollectTelemetry) + { + errorBuilder.SetExtension(WellKnownErrorExtensions.SourceOperationPlanNodeId, _node.Id); + } + + var error = errorBuilder.Build(); _context.AddErrors(error, _node._responseNames, Path.Root); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs index ad5d7812c60..c6a02873585 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs @@ -140,9 +140,11 @@ private static void WriteOperationNode( jsonWriter.WriteNumber("id", node.Id); jsonWriter.WriteString("type", node.Type.ToString()); - if (!string.IsNullOrEmpty(node.SchemaName)) + var schemaName = node.SchemaName ?? trace?.SchemaName; + + if (!string.IsNullOrEmpty(schemaName)) { - jsonWriter.WriteString("schema", node.SchemaName); + jsonWriter.WriteString("schema", schemaName); } jsonWriter.WriteStartObject("operation"); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs index d73580d7244..2564971ce96 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs @@ -96,9 +96,11 @@ private static void WriteOperationNode(OperationExecutionNode node, ExecutionNod writer.WriteLine("type: {0}", "Operation"); - if (node.SchemaName is not null) + var schemaName = node.SchemaName ?? trace?.SchemaName; + + if (!string.IsNullOrEmpty(schemaName)) { - writer.WriteLine("schema: {0}", node.SchemaName); + writer.WriteLine("schema: {0}", schemaName); } writer.WriteLine("operation: >-"); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index c652c1a01f7..1b6117eaa63 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -147,11 +147,23 @@ internal ImmutableArray GetVariableValueSets(ExecutionNode node) return []; } - return _nodeContexts.TryGetValue(node.Id, out var variableValueSets) - ? variableValueSets.Variables + return _nodeContexts.TryGetValue(node.Id, out var context) + ? context.Variables : []; } + internal string? GetSchemaName(ExecutionNode node) + { + if (!CollectTelemetry) + { + return null; + } + + return _nodeContexts.TryGetValue(node.Id, out var context) + ? context.SchemaName + : null; + } + internal void CompleteNode(ExecutionNodeResult result) => _executionState.EnqueueForCompletion(result); @@ -182,8 +194,9 @@ internal ImmutableArray CreateVariableValueSets( internal void AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, + ReadOnlySpan errors, ReadOnlySpan responseNames) - => _resultStore.AddPartialResults(sourcePath, results, responseNames); + => _resultStore.AddPartialResults(sourcePath, results, errors, responseNames); internal void AddPartialResults(ObjectResult result, ReadOnlySpan selections) => _resultStore.AddPartialResults(result, selections); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs index 9baf09d1d21..bac78febe57 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs @@ -186,7 +186,8 @@ async IAsyncEnumerable CreateResponseStream() eventArgs.Duration, Exception: null, DependentsToExecute: [], - VariableValueSets: eventArgs.VariableValueSets)); + VariableValueSets: eventArgs.VariableValueSets, + SchemaName: null)); while (!cancellationToken.IsCancellationRequested && executionState.IsProcessing()) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 68a2533f0a3..53919ca3103 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -68,6 +68,7 @@ public void Reset(ResultPoolSession resultPoolSession) public void AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, + ReadOnlySpan errors, ReadOnlySpan responseNames) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -88,6 +89,7 @@ public void AddPartialResults( try { ref var result = ref MemoryMarshal.GetReference(results); + ref var sourceSchemaError = ref MemoryMarshal.GetReference(errors); ref var dataElement = ref MemoryMarshal.GetReference(dataElementsSpan); ref var errorTrie = ref MemoryMarshal.GetReference(errorTriesSpan); ref var end = ref Unsafe.Add(ref result, results.Length); @@ -97,15 +99,16 @@ public void AddPartialResults( // we need to track the result objects as they used rented memory. _memory.Push(result); - if (result.Errors?.RootErrors is { Length: > 0 } rootErrors) + if (sourceSchemaError?.RootErrors is { Length: > 0 } rootErrors) { _errors.AddRange(rootErrors); } dataElement = GetDataElement(sourcePath, result.Data); - errorTrie = GetErrorTrie(sourcePath, result.Errors?.Trie); + errorTrie = GetErrorTrie(sourcePath, sourceSchemaError?.Trie); result = ref Unsafe.Add(ref result, 1)!; + sourceSchemaError = ref Unsafe.Add(ref sourceSchemaError, 1); dataElement = ref Unsafe.Add(ref dataElement, 1); errorTrie = ref Unsafe.Add(ref errorTrie, 1)!; } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/WellKnownErrorExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/WellKnownErrorExtensions.cs new file mode 100644 index 00000000000..449ab2142c3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/WellKnownErrorExtensions.cs @@ -0,0 +1,8 @@ +namespace HotChocolate.Fusion.Execution; + +public static class WellKnownErrorExtensions +{ + public const string OriginalIdValue = "originalValue"; + + public const string SourceOperationPlanNodeId = "sourceOperationPlanNodeId"; +}