Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 99 additions & 7 deletions src/Core/Services/BuildRequestStateMiddleware.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand All @@ -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;
}

/// <summary>
Expand All @@ -26,14 +35,97 @@ public BuildRequestStateMiddleware(RequestDelegate next)
/// <param name="context">HotChocolate execution request context.</param>
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[AuthorizationResolver.CLIENT_ROLE_HEADER].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);
}
}
}

34 changes: 33 additions & 1 deletion src/Core/Services/ExecutionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Text.Json;
Expand All @@ -9,6 +10,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.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder;
using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
Expand All @@ -19,6 +21,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
{
Expand Down Expand Up @@ -52,6 +55,8 @@ public ExecutionHelper(
/// </param>
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);
Expand Down Expand Up @@ -93,6 +98,8 @@ public async ValueTask ExecuteQueryAsync(IMiddlewareContext context)
/// </param>
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);
Expand Down Expand Up @@ -129,6 +136,31 @@ public async ValueTask ExecuteMutateAsync(IMiddlewareContext context)
}
}

/// <summary>
/// Starts the activity for the query
/// </summary>
/// <param name="context">
/// The middleware context.
/// </param>
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;
}

/// <summary>
/// Represents a pure resolver for a leaf field.
/// This resolver extracts the field value from the json object.
Expand Down Expand Up @@ -441,7 +473,7 @@ internal static IType InnerMostType(IType type)

public static InputObjectType InputObjectTypeFromIInputField(IInputField field)
{
return (InputObjectType)(InnerMostType(field.Type));
return (InputObjectType)InnerMostType(field.Type);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Helper class for tracking telemetry metrics such as active requests, errors, total requests,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -18,7 +17,7 @@ public static class TelemetryTracesHelper
public static readonly ActivitySource DABActivitySource = new("DataApiBuilder");

/// <summary>
/// Tracks the start of a REST controller activity.
/// Tracks the start of the main controller activity.
/// </summary>
/// <param name="activity">The activity instance.</param>
/// <param name="httpMethod">The HTTP method of the request (e.g., GET, POST).</param>
Expand All @@ -28,11 +27,11 @@ public static class TelemetryTracesHelper
/// <param name="queryString">The query string of the request, if any.</param>
/// <param name="userRole">The role of the user making the request.</param>
/// <param name="apiType">The type of API being used (e.g., REST, GraphQL).</param>
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,
Expand Down Expand Up @@ -78,11 +77,11 @@ public static void TrackQueryActivityStarted(
}

/// <summary>
/// Tracks the completion of a REST controller activity.
/// Tracks the completion of the main controller activity without any exceptions.
/// </summary>
/// <param name="activity">The activity instance.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
public static void TrackRestControllerActivityFinished(
public static void TrackMainControllerActivityFinished(
this Activity activity,
HttpStatusCode statusCode)
{
Expand All @@ -93,12 +92,12 @@ public static void TrackRestControllerActivityFinished(
}

/// <summary>
/// Tracks the completion of a REST controller activity with an exception.
/// Tracks the completion of the main controller activity with an exception.
/// </summary>
/// <param name="activity">The activity instance.</param>
/// <param name="ex">The exception that occurred.</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
public static void TrackRestControllerActivityFinishedWithException(
public static void TrackMainControllerActivityFinishedWithException(
this Activity activity,
Exception ex,
HttpStatusCode statusCode)
Expand Down
10 changes: 5 additions & 5 deletions src/Service/Controllers/RestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -208,7 +208,7 @@ private async Task<IActionResult> HandleOperation(

if (activity is not null)
{
activity.TrackRestControllerActivityStarted(
activity.TrackMainControllerActivityStarted(
Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true),
HttpContext.Request.Headers["User-Agent"].ToString(),
operationType.ToString(),
Expand Down Expand Up @@ -261,7 +261,7 @@ private async Task<IActionResult> HandleOperation(
if (activity is not null && activity.IsAllDataRequested)
{
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(statusCode.ToString(), ignoreCase: true);
activity.TrackRestControllerActivityFinished(httpStatusCode);
activity.TrackMainControllerActivityFinished(httpStatusCode);
}

return result;
Expand All @@ -274,7 +274,7 @@ private async Task<IActionResult> HandleOperation(
HttpContextExtensions.GetLoggerCorrelationId(HttpContext));

Response.StatusCode = (int)ex.StatusCode;
activity?.TrackRestControllerActivityFinishedWithException(ex, ex.StatusCode);
activity?.TrackMainControllerActivityFinishedWithException(ex, ex.StatusCode);

HttpMethod method = Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true);
TelemetryMetricsHelper.TrackError(method, ex.StatusCode, route, ApiType.REST, ex);
Expand All @@ -290,7 +290,7 @@ private async Task<IActionResult> HandleOperation(
Response.StatusCode = (int)HttpStatusCode.InternalServerError;

HttpMethod method = Enum.Parse<HttpMethod>(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(
Expand Down
1 change: 1 addition & 0 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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;
Expand Down
Loading