diff --git a/src/Core/Services/BuildRequestStateMiddleware.cs b/src/Core/Services/BuildRequestStateMiddleware.cs index 05d9eaa8a1..a7b8b21525 100644 --- a/src/Core/Services/BuildRequestStateMiddleware.cs +++ b/src/Core/Services/BuildRequestStateMiddleware.cs @@ -1,10 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; +using System.Net; +using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Authorization; +using Azure.DataApiBuilder.Core.Configurations; +using Azure.DataApiBuilder.Core.Telemetry; using HotChocolate.Execution; +using HotChocolate.Language; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; using RequestDelegate = HotChocolate.Execution.RequestDelegate; /// @@ -13,10 +20,12 @@ public sealed class BuildRequestStateMiddleware { private readonly RequestDelegate _next; + private readonly RuntimeConfigProvider _runtimeConfigProvider; - public BuildRequestStateMiddleware(RequestDelegate next) + public BuildRequestStateMiddleware(RequestDelegate next, RuntimeConfigProvider runtimeConfigProvider) { _next = next; + _runtimeConfigProvider = runtimeConfigProvider; } /// @@ -26,14 +35,97 @@ public BuildRequestStateMiddleware(RequestDelegate next) /// HotChocolate execution request context. public async ValueTask InvokeAsync(IRequestContext context) { - if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value) && - value is HttpContext httpContext) + bool isIntrospectionQuery = context.Request.OperationName == "IntrospectionQuery"; + ApiType apiType = ApiType.GraphQL; + Kestral method = Kestral.Post; + string route = _runtimeConfigProvider.GetConfig().GraphQLPath.Trim('/'); + DefaultHttpContext httpContext = (DefaultHttpContext)context.ContextData.First(x => x.Key == "HttpContext").Value!; + Stopwatch stopwatch = Stopwatch.StartNew(); + + using Activity? activity = !isIntrospectionQuery ? + TelemetryTracesHelper.DABActivitySource.StartActivity($"{method} /{route}") : null; + + try { - // Because Request.Headers is a NameValueCollection type, key not found will return StringValues.Empty and not an exception. - StringValues clientRoleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER]; - context.ContextData.TryAdd(key: AuthorizationResolver.CLIENT_ROLE_HEADER, value: clientRoleHeader); + // We want to ignore introspection queries DAB uses to check access to GraphQL since they are not sent by the user. + if (!isIntrospectionQuery) + { + TelemetryMetricsHelper.IncrementActiveRequests(apiType); + if (activity is not null) + { + activity.TrackMainControllerActivityStarted( + httpMethod: method, + userAgent: httpContext.Request.Headers["User-Agent"].ToString(), + actionType: (context.Request.Query!.ToString().Contains("mutation") ? OperationType.Mutation : OperationType.Query).ToString(), + httpURL: string.Empty, // GraphQL has no route + queryString: null, // GraphQL has no query-string + userRole: httpContext.Request.Headers["X-MS-API-ROLE"].FirstOrDefault() ?? httpContext.User.FindFirst("role")?.Value, + apiType: apiType); + } + } + + await InvokeAsync(); + } + finally + { + stopwatch.Stop(); + + HttpStatusCode statusCode; + + // We want to ignore introspection queries DAB uses to check access to GraphQL since they are not sent by the user. + if (!isIntrospectionQuery) + { + // There is an error in GraphQL when ContextData is not null + if (context.Result!.ContextData is not null) + { + if (context.Result.ContextData.ContainsKey(WellKnownContextData.ValidationErrors)) + { + statusCode = HttpStatusCode.BadRequest; + } + else if (context.Result.ContextData.ContainsKey(WellKnownContextData.OperationNotAllowed)) + { + statusCode = HttpStatusCode.MethodNotAllowed; + } + else + { + statusCode = HttpStatusCode.InternalServerError; + } + + Exception ex = new(); + if (context.Result.Errors is not null) + { + string errorMessage = context.Result.Errors[0].Message; + ex = new(errorMessage); + } + + // Activity will track error + activity?.TrackMainControllerActivityFinishedWithException(ex, statusCode); + TelemetryMetricsHelper.TrackError(method, statusCode, route, apiType, ex); + } + else + { + statusCode = HttpStatusCode.OK; + activity?.TrackMainControllerActivityFinished(statusCode); + } + + TelemetryMetricsHelper.TrackRequest(method, statusCode, route, apiType); + TelemetryMetricsHelper.TrackRequestDuration(method, statusCode, route, apiType, stopwatch.Elapsed); + TelemetryMetricsHelper.DecrementActiveRequests(apiType); + } } - await _next(context).ConfigureAwait(false); + async Task InvokeAsync() + { + if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value) && + value is HttpContext httpContext) + { + // Because Request.Headers is a NameValueCollection type, key not found will return StringValues.Empty and not an exception. + StringValues clientRoleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER]; + context.ContextData.TryAdd(key: AuthorizationResolver.CLIENT_ROLE_HEADER, value: clientRoleHeader); + } + + await _next(context).ConfigureAwait(false); + } } } + diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index b90873538b..16337d0223 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.Globalization; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; @@ -8,6 +9,7 @@ using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Resolvers.Factories; +using Azure.DataApiBuilder.Core.Telemetry; using Azure.DataApiBuilder.Service.GraphQLBuilder; using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars; using Azure.DataApiBuilder.Service.GraphQLBuilder.GraphQLTypes; @@ -17,6 +19,7 @@ using HotChocolate.Resolvers; using HotChocolate.Types.NodaTime; using NodaTime.Text; +using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; namespace Azure.DataApiBuilder.Service.Services { @@ -50,6 +53,8 @@ public ExecutionHelper( /// public async ValueTask ExecuteQueryAsync(IMiddlewareContext context) { + using Activity? activity = StartQueryActivity(context); + string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig()); DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType); @@ -91,6 +96,8 @@ public async ValueTask ExecuteQueryAsync(IMiddlewareContext context) /// public async ValueTask ExecuteMutateAsync(IMiddlewareContext context) { + using Activity? activity = StartQueryActivity(context); + string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig()); DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName); IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType); @@ -127,6 +134,31 @@ public async ValueTask ExecuteMutateAsync(IMiddlewareContext context) } } + /// + /// Starts the activity for the query + /// + /// + /// The middleware context. + /// + private Activity? StartQueryActivity(IMiddlewareContext context) + { + string route = _runtimeConfigProvider.GetConfig().GraphQLPath.Trim('/'); + Kestral method = Kestral.Post; + + Activity? activity = TelemetryTracesHelper.DABActivitySource.StartActivity($"{method} /{route}"); + + if (activity is not null) + { + string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig()); + DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName); + activity.TrackQueryActivityStarted( + databaseType: ds.DatabaseType, + dataSourceName: dataSourceName); + } + + return activity; + } + /// /// Represents a pure resolver for a leaf field. /// This resolver extracts the field value from the json object. @@ -425,7 +457,7 @@ internal static IType InnerMostType(IType type) public static InputObjectType InputObjectTypeFromIInputField(IInputField field) { - return (InputObjectType)(InnerMostType(field.Type)); + return (InputObjectType)InnerMostType(field.Type); } /// diff --git a/src/Service/Telemetry/TelemetryMetricsHelper.cs b/src/Core/Telemetry/TelemetryMetricsHelper.cs similarity index 97% rename from src/Service/Telemetry/TelemetryMetricsHelper.cs rename to src/Core/Telemetry/TelemetryMetricsHelper.cs index c8e61032b3..f43302052f 100644 --- a/src/Service/Telemetry/TelemetryMetricsHelper.cs +++ b/src/Core/Telemetry/TelemetryMetricsHelper.cs @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Net; using Azure.DataApiBuilder.Config.ObjectModel; using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; -namespace Azure.DataApiBuilder.Service.Telemetry +namespace Azure.DataApiBuilder.Core.Telemetry { /// /// Helper class for tracking telemetry metrics such as active requests, errors, total requests, diff --git a/src/Service/Telemetry/TelemetryTracesHelper.cs b/src/Core/Telemetry/TelemetryTracesHelper.cs similarity index 87% rename from src/Service/Telemetry/TelemetryTracesHelper.cs rename to src/Core/Telemetry/TelemetryTracesHelper.cs index 33a6029f43..01c5acbf51 100644 --- a/src/Service/Telemetry/TelemetryTracesHelper.cs +++ b/src/Core/Telemetry/TelemetryTracesHelper.cs @@ -1,14 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Diagnostics; using System.Net; using Azure.DataApiBuilder.Config.ObjectModel; using OpenTelemetry.Trace; using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod; -namespace Azure.DataApiBuilder.Service.Telemetry +namespace Azure.DataApiBuilder.Core.Telemetry { public static class TelemetryTracesHelper { @@ -18,7 +17,7 @@ public static class TelemetryTracesHelper public static readonly ActivitySource DABActivitySource = new("DataApiBuilder"); /// - /// Tracks the start of a REST controller activity. + /// Tracks the start of the main controller activity. /// /// The activity instance. /// The HTTP method of the request (e.g., GET, POST). @@ -28,11 +27,11 @@ public static class TelemetryTracesHelper /// The query string of the request, if any. /// The role of the user making the request. /// The type of API being used (e.g., REST, GraphQL). - public static void TrackRestControllerActivityStarted( + public static void TrackMainControllerActivityStarted( this Activity activity, Kestral httpMethod, string userAgent, - string actionType, + string actionType, // CRUD(EntityActionOperation) for REST, Query|Mutation(OperationType) for GraphQL string httpURL, string? queryString, string? userRole, @@ -78,11 +77,11 @@ public static void TrackQueryActivityStarted( } /// - /// Tracks the completion of a REST controller activity. + /// Tracks the completion of the main controller activity without any exceptions. /// /// The activity instance. /// The HTTP status code of the response. - public static void TrackRestControllerActivityFinished( + public static void TrackMainControllerActivityFinished( this Activity activity, HttpStatusCode statusCode) { @@ -93,12 +92,12 @@ public static void TrackRestControllerActivityFinished( } /// - /// Tracks the completion of a REST controller activity with an exception. + /// Tracks the completion of the main controller activity with an exception. /// /// The activity instance. /// The exception that occurred. /// The HTTP status code of the response. - public static void TrackRestControllerActivityFinishedWithException( + public static void TrackMainControllerActivityFinishedWithException( this Activity activity, Exception ex, HttpStatusCode statusCode) diff --git a/src/Service/Controllers/RestController.cs b/src/Service/Controllers/RestController.cs index 10e1f4263d..cebd6f4463 100644 --- a/src/Service/Controllers/RestController.cs +++ b/src/Service/Controllers/RestController.cs @@ -11,8 +11,8 @@ using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Services; +using Azure.DataApiBuilder.Core.Telemetry; using Azure.DataApiBuilder.Service.Exceptions; -using Azure.DataApiBuilder.Service.Telemetry; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -208,7 +208,7 @@ private async Task HandleOperation( if (activity is not null) { - activity.TrackRestControllerActivityStarted( + activity.TrackMainControllerActivityStarted( Enum.Parse(HttpContext.Request.Method, ignoreCase: true), HttpContext.Request.Headers["User-Agent"].ToString(), operationType.ToString(), @@ -261,7 +261,7 @@ private async Task HandleOperation( if (activity is not null && activity.IsAllDataRequested) { HttpStatusCode httpStatusCode = Enum.Parse(statusCode.ToString(), ignoreCase: true); - activity.TrackRestControllerActivityFinished(httpStatusCode); + activity.TrackMainControllerActivityFinished(httpStatusCode); } return result; @@ -274,7 +274,7 @@ private async Task HandleOperation( HttpContextExtensions.GetLoggerCorrelationId(HttpContext)); Response.StatusCode = (int)ex.StatusCode; - activity?.TrackRestControllerActivityFinishedWithException(ex, ex.StatusCode); + activity?.TrackMainControllerActivityFinishedWithException(ex, ex.StatusCode); HttpMethod method = Enum.Parse(HttpContext.Request.Method, ignoreCase: true); TelemetryMetricsHelper.TrackError(method, ex.StatusCode, route, ApiType.REST, ex); @@ -290,7 +290,7 @@ private async Task HandleOperation( Response.StatusCode = (int)HttpStatusCode.InternalServerError; HttpMethod method = Enum.Parse(HttpContext.Request.Method, ignoreCase: true); - activity?.TrackRestControllerActivityFinishedWithException(ex, HttpStatusCode.InternalServerError); + activity?.TrackMainControllerActivityFinishedWithException(ex, HttpStatusCode.InternalServerError); TelemetryMetricsHelper.TrackError(method, HttpStatusCode.InternalServerError, route, ApiType.REST, ex); return ErrorResponse( diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index a0d3ef3393..5192d74537 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -21,6 +21,7 @@ using Azure.DataApiBuilder.Core.Services.Cache; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Core.Services.OpenAPI; +using Azure.DataApiBuilder.Core.Telemetry; using Azure.DataApiBuilder.Service.Controllers; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.HealthCheck;