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 children)) + { + children = []; + parent["activities"] = children; + } + + var data = new OrderedDictionary(); + activity.SetCustomProperty("test.data", data); + SerializeActivity(activity); + children.Add(data); + } + + private static void SerializeActivity(Activity activity) + { + var data = (OrderedDictionary?)activity.GetCustomProperty("test.data"); + + if (data is null) + { + return; + } + + data["OperationName"] = activity.OperationName; + data["DisplayName"] = activity.DisplayName; + data["Status"] = activity.Status; + data["tags"] = activity.Tags; + data["event"] = activity.Events.Select(t => new + { + t.Name, + Tags = ScrubEventTags(t.Tags) + }); + } + + private static IEnumerable> ScrubEventTags( + IEnumerable> tags) + { + foreach (var tag in tags) + { + if (tag is { Key: "exception.stacktrace", Value: string stackTrace }) + { + yield return new KeyValuePair( + tag.Key, + StackTracePathRegex().Replace(stackTrace, match => + { + var fileName = System.IO.Path.GetFileName(match.Groups[1].Value); + var lineNumber = match.Groups[2].Value; + return $" in {fileName}:line {lineNumber}"; + })); + } + else + { + yield return tag; + } + } + } + + [GeneratedRegex(@" in (.+):line (\d+)")] + private static partial Regex StackTracePathRegex(); + + private sealed class Session : IDisposable + { + private readonly Activity _activity; + private readonly ActivityListener _listener; + + public Session(Activity activity, ActivityListener listener) + { + _activity = activity; + _listener = listener; + } + + public void Dispose() + { + _activity.Dispose(); + _listener.Dispose(); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/HotChocolate.Fusion.Diagnostics.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/HotChocolate.Fusion.Diagnostics.Tests.csproj new file mode 100644 index 00000000000..528f2642747 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/HotChocolate.Fusion.Diagnostics.Tests.csproj @@ -0,0 +1,14 @@ + + + + + HotChocolate.Fusion.Diagnostics.Tests + HotChocolate.FusionDiagnostics + + + + + + + + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/QueryInstrumentationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/QueryInstrumentationTests.cs new file mode 100644 index 00000000000..333c442cb85 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/QueryInstrumentationTests.cs @@ -0,0 +1,626 @@ +using System.Diagnostics; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; +using static HotChocolate.Fusion.Diagnostics.ActivityTestHelper; + +namespace HotChocolate.Fusion.Diagnostics; + +[Collection("Instrumentation")] +public class QueryInstrumentationTests : FusionTestBase +{ + [Fact] + public async Task Track_Events_Of_A_Simple_Query_Default() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation()); + + // act + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Track_Events_Of_A_Simple_Query_Default_Rename_Root() + { + using (CaptureActivities(out _)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + Assert.Equal("CaptureActivities: query { sayHello }", Activity.Current!.DisplayName); + } + } + + [Fact] + public async Task Parsing_Error_When_Rename_Root_Is_Activated() + { + using (CaptureActivities(out _)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + Assert.Equal("CaptureActivities: Begin Parse Document", Activity.Current!.DisplayName); + } + } + + [Fact] + public async Task Validation_Error_When_Rename_Root_Is_Activated() + { + using (CaptureActivities(out _)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ abc123 }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + Assert.Equal("CaptureActivities: Begin Validate Document", + Activity.Current!.DisplayName); + } + } + + [Fact] + public async Task Create_Operation_Display_Name_With_1_Field() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ a: sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Create_Operation_Display_Name_With_1_Field_And_Op() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("query GetA { a: sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Create_Operation_Display_Name_With_3_Field() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ a: sayHello b: sayHello c: sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Create_Operation_Display_Name_With_4_Field() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.RenameRootActivity = true; + o.Scopes = FusionActivityScopes.All; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ a: sayHello b: sayHello c: sayHello d: sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Track_Events_Of_A_Simple_Query_Detailed() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Ensure_Operation_Name_Is_Used_As_Request_Name() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("query SayHelloOperation { sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Allow_Document_To_Be_Captured() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.Scopes = FusionActivityScopes.All; + o.IncludeDocument = true; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("query SayHelloOperation { sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Ensure_That_The_Validation_Activity_Has_An_Error_Status() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.Scopes = FusionActivityScopes.All; + o.IncludeDocument = true; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("query SayHelloOperation { sayHello_ }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Cause_A_Resolver_Error_That_Deletes_The_Whole_Result() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.Scopes = FusionActivityScopes.All; + o.IncludeDocument = true; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("query SayHelloOperation { causeFatalError }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Cause_A_Resolver_Error_That_Deletes_The_Whole_Result_Deep() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.Scopes = FusionActivityScopes.All; + o.IncludeDocument = true; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument( + """ + query SayHelloOperation { + deep { + deeper { + deeps { + deeper { + causeFatalError + } + } + } + } + } + """) + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Track_Events_Of_A_Simple_Query_With_Node_Scopes() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Source_Schema_Transport_Error() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType(), + isOffline: true); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + { + o.Scopes = FusionActivityScopes.All; + o.IncludeDocument = true; + })); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Track_Events_Of_A_Query_With_Multiple_Sources() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server1 = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var server2 = CreateSourceSchema( + "b", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", server1), + ("b", server2) + ], + configureGatewayBuilder: b => b.AddInstrumentation(o => + o.Scopes = FusionActivityScopes.All)); + + var executor = await gateway.Services.GetRequestExecutorAsync(); + + var request = OperationRequestBuilder.New() + .SetDocument("{ sayHello sayGoodbye }") + .Build(); + + // act + await executor.ExecuteAsync(request); + + // assert + activities.MatchSnapshot(); + } + } + + public class Query + { + public string SayHello() => "hello"; + + public string CauseFatalError() => throw new GraphQLException("fail"); + + public Deep Deep() => new(); + } + + [GraphQLName("Query")] + public class QueryA + { + public string SayHello() => "hello"; + } + + [GraphQLName("Query")] + public class QueryB + { + public string SayGoodbye() => "goodbye"; + } + + public class Deep + { + public Deeper Deeper() => new(); + + public string CauseFatalError() => throw new GraphQLException("fail"); + } + + public class Deeper + { + public Deep[] Deeps() => [new Deep()]; + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ServerInstrumentationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ServerInstrumentationTests.cs new file mode 100644 index 00000000000..2631a4e88bc --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/ServerInstrumentationTests.cs @@ -0,0 +1,417 @@ +using HotChocolate.Transport.Http; +using Microsoft.Extensions.DependencyInjection; +using static HotChocolate.Fusion.Diagnostics.ActivityTestHelper; +using OperationRequest = HotChocolate.Transport.OperationRequest; + +namespace HotChocolate.Fusion.Diagnostics; + +[Collection("Instrumentation")] +public class ServerInstrumentationTests : FusionTestBase +{ + private static readonly Uri _url = new("http://localhost:5000/graphql"); + + [Fact] + public async Task Http_Post_Single_Request_Default() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation()); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest("{ sayHello }"); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_Single_Request() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest("{ sayHello }"); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Get_Single_Request() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest("{ sayHello }"); + + // act + using var result = await client.GetAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_Variables_Are_Not_Automatically_Added_To_Activities() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + query: """ + query ($name: String!) { + greeting(name: $name) + } + """, + variables: new Dictionary { { "name", "World" } }); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_Add_Variables_To_Http_Activity() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => + { + o.Scopes = FusionActivityScopes.All; + o.RequestDetails = RequestDetails.Default | RequestDetails.Variables; + })); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + query: """ + query ($name: String!) { + greeting(name: $name) + } + """, + variables: new Dictionary { { "name", "World" } }); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_Add_Query_To_Http_Activity() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => + { + o.Scopes = FusionActivityScopes.All; + o.RequestDetails = RequestDetails.Default | RequestDetails.Operation; + })); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + query: """ + query ($name: String!) { + greeting(name: $name) + } + """, + variables: new Dictionary { { "name", "World" } }); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_With_Extensions_Map() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + query: """ + query ($name: String!) { + greeting(name: $name) + } + """, + variables: new Dictionary { { "name", "World" } }, + extensions: new Dictionary { { "test", "abc" } }); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Get_SDL_Download() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + var httpClient = gateway.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/graphql?sdl"); + + // act + var response = await httpClient.SendAsync(request); + + // assert + await response.Content.ReadAsStringAsync(); + activities.MatchSnapshot(); + } + } + + [Fact(Skip = "Not yet implemented")] + public async Task Http_Post_Capture_Deferred_Response() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest( + """ + TODO + """); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Http_Post_Parser_Error() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => o.Scopes = FusionActivityScopes.All)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + // lang=text + var request = new OperationRequest( + """ + { + deep { + deeper { + 1deeps { + name + } + } + } + } + """); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Parsing_Error_When_Rename_Root_Is_Activated() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => + { + o.Scopes = FusionActivityScopes.All; + o.RenameRootActivity = true; + })); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + // lang=text + var request = new OperationRequest("{ 1 }"); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + [Fact] + public async Task Validation_Error_When_Rename_Root_Is_Activated() + { + using (CaptureActivities(out var activities)) + { + // arrange + using var server = CreateSourceSchema( + "a", + b => b.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [("a", server)], + configureGatewayBuilder: b => b.AddInstrumentation( + o => + { + o.Scopes = FusionActivityScopes.All; + o.RenameRootActivity = true; + })); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var request = new OperationRequest("{ abc }"); + + // act + using var result = await client.PostAsync(request, _url); + + // assert + activities.MatchSnapshot(); + } + } + + public class Query + { + public string SayHello() => "hello"; + + public string Greeting(string name) => $"Hello, {name}!"; + + public string CauseFatalError() => throw new GraphQLException("fail"); + + public Deep Deep() => new(); + } + + public class Deep + { + public string Name => "deep"; + + public Deeper Deeper() => new(); + } + + public class Deeper + { + public string Name => "deeper"; + + public Deep[] Deeps() => [new Deep()]; + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Allow_Document_To_Be_Captured.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Allow_Document_To_Be_Captured.snap new file mode 100644 index 00000000000..d3e100c155c --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Allow_Document_To_Be_Captured.snap @@ -0,0 +1,112 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query SayHelloOperation { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.document.hash", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.operation.id", + "Value": "1334fb0da1250c6db5db84b6c98ccb2556f066942f8836d6ebd18fd870172787" + }, + { + "Key": "graphql.operation.name", + "Value": "SayHelloOperation" + }, + { + "Key": "graphql.document.body", + "Value": "query SayHelloOperation {\n sayHello\n}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.document.hash", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation SayHelloOperation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result.snap new file mode 100644 index 00000000000..86e0de8de0f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result.snap @@ -0,0 +1,116 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query SayHelloOperation { causeFatalError }", + "Status": "Error", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "851fb754d9ba6b5cc5a55ebcbea2621d" + }, + { + "Key": "graphql.document.hash", + "Value": "851fb754d9ba6b5cc5a55ebcbea2621d" + }, + { + "Key": "graphql.operation.id", + "Value": "5f75eb886568e255310bed3eb3e1f7f1c91f1a22f71ac7c36f00d8df27400d8e" + }, + { + "Key": "graphql.operation.name", + "Value": "SayHelloOperation" + }, + { + "Key": "graphql.document.body", + "Value": "query SayHelloOperation {\n causeFatalError\n}" + }, + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "851fb754d9ba6b5cc5a55ebcbea2621d" + }, + { + "Key": "graphql.document.hash", + "Value": "851fb754d9ba6b5cc5a55ebcbea2621d" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation SayHelloOperation", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result_Deep.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result_Deep.snap new file mode 100644 index 00000000000..158121c23ce --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Cause_A_Resolver_Error_That_Deletes_The_Whole_Result_Deep.snap @@ -0,0 +1,84 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "Execute Request", + "Status": "Error", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "803df9346db185e9dc0b22dd3909aa70" + }, + { + "Key": "graphql.document.hash", + "Value": "803df9346db185e9dc0b22dd3909aa70" + }, + { + "Key": "graphql.document.body", + "Value": "query SayHelloOperation {\n deep {\n deeper {\n deeps {\n deeper {\n causeFatalError\n }\n }\n }\n }\n}" + }, + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + }, + { + "Key": "graphql.document.id", + "Value": "803df9346db185e9dc0b22dd3909aa70" + }, + { + "Key": "graphql.document.hash", + "Value": "803df9346db185e9dc0b22dd3909aa70" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "The field `causeFatalError` does not exist on the type `Deeper`." + }, + { + "Key": "exception.type", + "Value": "GRAPHQL_ERROR" + }, + { + "Key": "graphql.error.location.column", + "Value": 21 + }, + { + "Key": "graphql.error.location.line", + "Value": 6 + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field.snap new file mode 100644 index 00000000000..85a94d3f7b2 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field.snap @@ -0,0 +1,104 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { a }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "452ea802c4d1bf2a81a7411b0b361d9f" + }, + { + "Key": "graphql.document.hash", + "Value": "452ea802c4d1bf2a81a7411b0b361d9f" + }, + { + "Key": "graphql.operation.id", + "Value": "91d3f369067488892e5c81c27598c0d43b5ecfe5ad824925965ac60c70351919" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "452ea802c4d1bf2a81a7411b0b361d9f" + }, + { + "Key": "graphql.document.hash", + "Value": "452ea802c4d1bf2a81a7411b0b361d9f" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field_And_Op.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field_And_Op.snap new file mode 100644 index 00000000000..0ed0deda928 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_1_Field_And_Op.snap @@ -0,0 +1,108 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query GetA { a }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "cee0e2939ece72d650cb0331f4be4669" + }, + { + "Key": "graphql.document.hash", + "Value": "cee0e2939ece72d650cb0331f4be4669" + }, + { + "Key": "graphql.operation.id", + "Value": "155189958e55686347a7f921c0f7a1ef143f829f5b116365a297651606d5703f" + }, + { + "Key": "graphql.operation.name", + "Value": "GetA" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "cee0e2939ece72d650cb0331f4be4669" + }, + { + "Key": "graphql.document.hash", + "Value": "cee0e2939ece72d650cb0331f4be4669" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation GetA", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_3_Field.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_3_Field.snap new file mode 100644 index 00000000000..f4870f0ca44 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_3_Field.snap @@ -0,0 +1,104 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { a b c }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "2e55fbe10a9e3ddf26935a8f8d15ec89" + }, + { + "Key": "graphql.document.hash", + "Value": "2e55fbe10a9e3ddf26935a8f8d15ec89" + }, + { + "Key": "graphql.operation.id", + "Value": "8b26a2633c9b68833461d6b2249f54600493dfc632399469d5c108e79410ca7b" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "2e55fbe10a9e3ddf26935a8f8d15ec89" + }, + { + "Key": "graphql.document.hash", + "Value": "2e55fbe10a9e3ddf26935a8f8d15ec89" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_4_Field.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_4_Field.snap new file mode 100644 index 00000000000..1af065e2ac9 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Create_Operation_Display_Name_With_4_Field.snap @@ -0,0 +1,104 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { a b c ... }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "a5f924bb2f5f8651014e92e1cc2428c7" + }, + { + "Key": "graphql.document.hash", + "Value": "a5f924bb2f5f8651014e92e1cc2428c7" + }, + { + "Key": "graphql.operation.id", + "Value": "1c020b5562fde1e7673b1f4750bdf6d35f2789819d03bb6b5d088bf445231501" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "a5f924bb2f5f8651014e92e1cc2428c7" + }, + { + "Key": "graphql.document.hash", + "Value": "a5f924bb2f5f8651014e92e1cc2428c7" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_Operation_Name_Is_Used_As_Request_Name.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_Operation_Name_Is_Used_As_Request_Name.snap new file mode 100644 index 00000000000..b58f55ac83f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_Operation_Name_Is_Used_As_Request_Name.snap @@ -0,0 +1,108 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query SayHelloOperation { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.document.hash", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.operation.id", + "Value": "1334fb0da1250c6db5db84b6c98ccb2556f066942f8836d6ebd18fd870172787" + }, + { + "Key": "graphql.operation.name", + "Value": "SayHelloOperation" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "graphql.document.hash", + "Value": "6af18618ae20c266f6ffc352b78cb69b" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation SayHelloOperation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_That_The_Validation_Activity_Has_An_Error_Status.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_That_The_Validation_Activity_Has_An_Error_Status.snap new file mode 100644 index 00000000000..725e9a44a05 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Ensure_That_The_Validation_Activity_Has_An_Error_Status.snap @@ -0,0 +1,84 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "Execute Request", + "Status": "Error", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bb1d246465341a97bdc727d6cd8ead5c" + }, + { + "Key": "graphql.document.hash", + "Value": "bb1d246465341a97bdc727d6cd8ead5c" + }, + { + "Key": "graphql.document.body", + "Value": "query SayHelloOperation {\n sayHello_\n}" + }, + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + }, + { + "Key": "graphql.document.id", + "Value": "bb1d246465341a97bdc727d6cd8ead5c" + }, + { + "Key": "graphql.document.hash", + "Value": "bb1d246465341a97bdc727d6cd8ead5c" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "The field `sayHello_` does not exist on the type `Query`." + }, + { + "Key": "exception.type", + "Value": "GRAPHQL_ERROR" + }, + { + "Key": "graphql.error.location.column", + "Value": 27 + }, + { + "Key": "graphql.error.location.line", + "Value": 1 + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap new file mode 100644 index 00000000000..c375ff8f2d5 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Source_Schema_Transport_Error.snap @@ -0,0 +1,130 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello }", + "Status": "Error", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + }, + { + "Key": "graphql.document.body", + "Value": "{\n sayHello\n}" + }, + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.type", + "Value": "System.Net.Http.HttpRequestException" + }, + { + "Key": "exception.stacktrace", + "Value": "System.Net.Http.HttpRequestException: Response status code does not indicate success: 500 (Internal Server Error).\n at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()\n at HotChocolate.Fusion.Transport.Http.GraphQLHttpResponse.ReadAsResultAsync(CancellationToken cancellationToken) in GraphQLHttpResponse.cs:line 150\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+MoveNext() in SourceSchemaHttpClient.cs:line 581\n at HotChocolate.Fusion.Execution.Clients.SourceSchemaHttpClient.Response.ReadAsResultStreamAsync(CancellationToken cancellationToken)+System.Threading.Tasks.Sources.IValueTaskSource.GetResult()\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158\n at HotChocolate.Fusion.Execution.Nodes.OperationExecutionNode.OnExecuteAsync(OperationPlanContext context, CancellationToken cancellationToken) in OperationExecutionNode.cs:line 158" + }, + { + "Key": "exception.message", + "Value": "Response status code does not indicate success: 500 (Internal Server Error)." + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Query_With_Multiple_Sources.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Query_With_Multiple_Sources.snap new file mode 100644 index 00000000000..fa4b3838f11 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Query_With_Multiple_Sources.snap @@ -0,0 +1,124 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello sayGoodbye }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "073bf7696c078e52587c88890ef21bbe" + }, + { + "Key": "graphql.document.hash", + "Value": "073bf7696c078e52587c88890ef21bbe" + }, + { + "Key": "graphql.operation.id", + "Value": "9babcd211d7b162261fa15a119462370a3f30c61ea319946c30bc4051a265a5d" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "073bf7696c078e52587c88890ef21bbe" + }, + { + "Key": "graphql.document.hash", + "Value": "073bf7696c078e52587c88890ef21bbe" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (b)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "b" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Default.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Default.snap new file mode 100644 index 00000000000..5ecaa32bc36 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Default.snap @@ -0,0 +1,56 @@ +{ + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Detailed.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Detailed.snap new file mode 100644 index 00000000000..c2b6eb5d039 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_Detailed.snap @@ -0,0 +1,104 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_With_Node_Scopes.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_With_Node_Scopes.snap new file mode 100644 index 00000000000..c2b6eb5d039 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/QueryInstrumentationTests.Track_Events_Of_A_Simple_Query_With_Node_Scopes.snap @@ -0,0 +1,104 @@ +{ + "activities": [ + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseDocument", + "DisplayName": "Parse Document", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_SDL_Download.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_SDL_Download.snap new file mode 100644 index 00000000000..1669a6a2090 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_SDL_Download.snap @@ -0,0 +1,16 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP GET SDL", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + } + ], + "event": [] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_Single_Request.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_Single_Request.snap new file mode 100644 index 00000000000..bb203ba5752 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Get_Single_Request.snap @@ -0,0 +1,138 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP GET", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Query_To_Http_Activity.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Query_To_Http_Activity.snap new file mode 100644 index 00000000000..1f787a37f1b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Query_To_Http_Activity.snap @@ -0,0 +1,150 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { greeting }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "graphql.operation.id", + "Value": "d58281f7cf44ca2751c4a435c0249e686bd1c146f6ddae23ed35ec6e4b83eb77" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "CoerceVariables", + "DisplayName": "Coerce Variable", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Variables_To_Http_Activity.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Variables_To_Http_Activity.snap new file mode 100644 index 00000000000..46c0ade8f33 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Add_Variables_To_Http_Activity.snap @@ -0,0 +1,154 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + }, + { + "Key": "graphql.http.request.variables", + "Value": "{\"name\":\"World\"}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { greeting }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "graphql.operation.id", + "Value": "d58281f7cf44ca2751c4a435c0249e686bd1c146f6ddae23ed35ec6e4b83eb77" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "CoerceVariables", + "DisplayName": "Coerce Variable", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Parser_Error.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Parser_Error.snap new file mode 100644 index 00000000000..28a52eca1ac --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Parser_Error.snap @@ -0,0 +1,59 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "Found a NameStart character `d` (100) following a number, which is disallowed." + }, + { + "Key": "exception.type", + "Value": "HC0011" + }, + { + "Key": "graphql.error.location.column", + "Value": 13 + }, + { + "Key": "graphql.error.location.line", + "Value": 4 + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request.snap new file mode 100644 index 00000000000..6badf966ba3 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request.snap @@ -0,0 +1,129 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "ExecuteHttpRequest", + "Status": "Unset", + "tags": [], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { sayHello }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request_Default.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request_Default.snap new file mode 100644 index 00000000000..86acae93e01 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Single_Request_Default.snap @@ -0,0 +1,114 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "query { sayHello }", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + }, + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.operation.id", + "Value": "456132b93ebaf15a39534753bf72f9f4bfa1152a08d04bc8a88539feec1cb52c" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "graphql.document.hash", + "Value": "f7e9989fbb67af7fa747a9983313c9e5" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Variables_Are_Not_Automatically_Added_To_Activities.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Variables_Are_Not_Automatically_Added_To_Activities.snap new file mode 100644 index 00000000000..1f787a37f1b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_Variables_Are_Not_Automatically_Added_To_Activities.snap @@ -0,0 +1,150 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { greeting }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "graphql.operation.id", + "Value": "d58281f7cf44ca2751c4a435c0249e686bd1c146f6ddae23ed35ec6e4b83eb77" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "CoerceVariables", + "DisplayName": "Coerce Variable", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_With_Extensions_Map.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_With_Extensions_Map.snap new file mode 100644 index 00000000000..2baf19b2df6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Http_Post_With_Extensions_Map.snap @@ -0,0 +1,154 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + }, + { + "Key": "graphql.http.request.extensions", + "Value": "{\"test\":\"abc\"}" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "query { greeting }", + "Status": "Unset", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "graphql.operation.id", + "Value": "d58281f7cf44ca2751c4a435c0249e686bd1c146f6ddae23ed35ec6e4b83eb77" + } + ], + "event": [ + { + "Name": "AddedOperationPlanToCache", + "Tags": [] + }, + { + "Name": "AddedDocumentToCache", + "Tags": [] + } + ], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "bfa5986a5299f46421057dd3eb27ec5c" + }, + { + "Key": "graphql.document.hash", + "Value": "c46cf8c9811934ddea095f10ee722dc4" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "PlanOperation", + "DisplayName": "Plan Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "CoerceVariables", + "DisplayName": "Coerce Variable", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteOperation", + "DisplayName": "Execute Operation", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ExecuteOperationNode", + "DisplayName": "Execute Operation Node (a)", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.fusion.node.type", + "Value": "Operation" + }, + { + "Key": "graphql.fusion.node.schema", + "Value": "a" + }, + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Parsing_Error_When_Rename_Root_Is_Activated.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Parsing_Error_When_Rename_Root_Is_Activated.snap new file mode 100644 index 00000000000..71a7c84c3a4 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Parsing_Error_When_Rename_Root_Is_Activated.snap @@ -0,0 +1,59 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST: Begin Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "Expected a `Name`-token, but found a `Integer`-token." + }, + { + "Key": "exception.type", + "Value": "HC0011" + }, + { + "Key": "graphql.error.location.column", + "Value": 3 + }, + { + "Key": "graphql.error.location.line", + "Value": 1 + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Validation_Error_When_Rename_Root_Is_Activated.snap b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Validation_Error_When_Rename_Root_Is_Activated.snap new file mode 100644 index 00000000000..a8e45a9ef6d --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Diagnostics.Tests/__snapshots__/ServerInstrumentationTests.Validation_Error_When_Rename_Root_Is_Activated.snap @@ -0,0 +1,105 @@ +{ + "activities": [ + { + "OperationName": "ExecuteHttpRequest", + "DisplayName": "GraphQL HTTP POST: Begin Validate Document", + "Status": "Ok", + "tags": [ + { + "Key": "graphql.schema.name", + "Value": "_Default" + }, + { + "Key": "graphql.http.request.type", + "Value": "single" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ParseHttpRequest", + "DisplayName": "Parse HTTP Request", + "Status": "Ok", + "tags": [ + { + "Key": "otel.status_code", + "Value": "OK" + } + ], + "event": [] + }, + { + "OperationName": "ExecuteRequest", + "DisplayName": "Execute Request", + "Status": "Error", + "tags": [ + { + "Key": "graphql.document.id", + "Value": "346f68539881f0624dca2927281d1a2f" + }, + { + "Key": "graphql.document.hash", + "Value": "346f68539881f0624dca2927281d1a2f" + }, + { + "Key": "otel.status_code", + "Value": "ERROR" + } + ], + "event": [], + "activities": [ + { + "OperationName": "ValidateDocument", + "DisplayName": "Validate Document", + "Status": "Error", + "tags": [ + { + "Key": "otel.status_code", + "Value": "ERROR" + }, + { + "Key": "graphql.document.id", + "Value": "346f68539881f0624dca2927281d1a2f" + }, + { + "Key": "graphql.document.hash", + "Value": "346f68539881f0624dca2927281d1a2f" + } + ], + "event": [ + { + "Name": "exception", + "Tags": [ + { + "Key": "exception.message", + "Value": "The field `abc` does not exist on the type `Query`." + }, + { + "Key": "exception.type", + "Value": "GRAPHQL_ERROR" + }, + { + "Key": "graphql.error.location.column", + "Value": 3 + }, + { + "Key": "graphql.error.location.line", + "Value": 1 + } + ] + } + ] + } + ] + }, + { + "OperationName": "FormatHttpResponse", + "DisplayName": "Format HTTP Response", + "Status": "Ok", + "tags": [], + "event": [] + } + ] + } + ] +}