Skip to content

Commit 7f8fa6b

Browse files
RubenCerna2079tommasodotNETJerry Nixon
authored
Enhance GraphQL OTEL instrumentation with custom metrics and traces (#2673)
## Why make this change? This closes #2642 ## What is this change? This PR enhances the OTEL instrumentation for the GraphQL APIs by adding custom traces and metrics. Metrics can be filtered for `status_code`, `api_type`, `endpoint` and `method`. ## How was this tested? - [X] Local Testing - [ ] Integration Tests - [ ] Unit Tests All of the tests were done locally to check if the log information that was provided was correct, for both scenarios in which the query gave the proper information or when an exception was raised. Another thing that was tested is that when we open GraphQL it would send a few requests called `Introspection Queries` used to ensure that GraphQL is working properly. However, we do not want the user to see these requests as part of the total count as this is done automatically, which may confuse the users. ## Sample Request(s) ![image](https://github.com/user-attachments/assets/1f66da36-d537-47cc-95d6-fd19cf73242e) ![image](https://github.com/user-attachments/assets/91b3801f-b48e-4841-99a6-72de1b8b482a) ![image](https://github.com/user-attachments/assets/48a8ee9a-4d17-4b52-9f66-7e5c745aeef4) ## Instructions on how to use DAB Workbench - Clone the following repo `https://github.com/tommasodotNET/dab-workbench.git` - Run your DAB version in CLI so the files from the `out` folder are created, and make sure to stop it before running the DAB Workbench since both cannot be running at the same time. - Find the path to the `Microsoft.DataApiBuilder.exe`, which should look something like `<PATH_TO_REPO>\data-api-builder\src\out\cli\net8.0\Microsoft.DataApiBuilder.exe` - Copy the path of the `.exe` file and paste it in the file `/DABWorkbench.AppHost/Program.cs` in the variable `dabCLIPath` which is found in line 3 as follows: `var dabCLIPath = @"<PATH_TO_REPO>\data-api-builder\src\out\cli\net8.0\Microsoft.DataApiBuilder.exe";` - Now you should be able to run DAB Workbench with your version of DAB. --------- Co-authored-by: Tommaso Stocchi <[email protected]> Co-authored-by: Jerry Nixon <[email protected]> Co-authored-by: Ruben Cerna <[email protected]>
1 parent 61c2a85 commit 7f8fa6b

File tree

6 files changed

+147
-25
lines changed

6 files changed

+147
-25
lines changed
Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Diagnostics;
5+
using System.Net;
6+
using Azure.DataApiBuilder.Config.ObjectModel;
47
using Azure.DataApiBuilder.Core.Authorization;
8+
using Azure.DataApiBuilder.Core.Configurations;
9+
using Azure.DataApiBuilder.Core.Telemetry;
510
using HotChocolate.Execution;
11+
using HotChocolate.Language;
612
using Microsoft.AspNetCore.Http;
713
using Microsoft.Extensions.Primitives;
14+
using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
815
using RequestDelegate = HotChocolate.Execution.RequestDelegate;
916

1017
/// <summary>
@@ -13,10 +20,12 @@
1320
public sealed class BuildRequestStateMiddleware
1421
{
1522
private readonly RequestDelegate _next;
23+
private readonly RuntimeConfigProvider _runtimeConfigProvider;
1624

17-
public BuildRequestStateMiddleware(RequestDelegate next)
25+
public BuildRequestStateMiddleware(RequestDelegate next, RuntimeConfigProvider runtimeConfigProvider)
1826
{
1927
_next = next;
28+
_runtimeConfigProvider = runtimeConfigProvider;
2029
}
2130

2231
/// <summary>
@@ -26,14 +35,97 @@ public BuildRequestStateMiddleware(RequestDelegate next)
2635
/// <param name="context">HotChocolate execution request context.</param>
2736
public async ValueTask InvokeAsync(IRequestContext context)
2837
{
29-
if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value) &&
30-
value is HttpContext httpContext)
38+
bool isIntrospectionQuery = context.Request.OperationName == "IntrospectionQuery";
39+
ApiType apiType = ApiType.GraphQL;
40+
Kestral method = Kestral.Post;
41+
string route = _runtimeConfigProvider.GetConfig().GraphQLPath.Trim('/');
42+
DefaultHttpContext httpContext = (DefaultHttpContext)context.ContextData.First(x => x.Key == "HttpContext").Value!;
43+
Stopwatch stopwatch = Stopwatch.StartNew();
44+
45+
using Activity? activity = !isIntrospectionQuery ?
46+
TelemetryTracesHelper.DABActivitySource.StartActivity($"{method} /{route}") : null;
47+
48+
try
3149
{
32-
// Because Request.Headers is a NameValueCollection type, key not found will return StringValues.Empty and not an exception.
33-
StringValues clientRoleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER];
34-
context.ContextData.TryAdd(key: AuthorizationResolver.CLIENT_ROLE_HEADER, value: clientRoleHeader);
50+
// We want to ignore introspection queries DAB uses to check access to GraphQL since they are not sent by the user.
51+
if (!isIntrospectionQuery)
52+
{
53+
TelemetryMetricsHelper.IncrementActiveRequests(apiType);
54+
if (activity is not null)
55+
{
56+
activity.TrackMainControllerActivityStarted(
57+
httpMethod: method,
58+
userAgent: httpContext.Request.Headers["User-Agent"].ToString(),
59+
actionType: (context.Request.Query!.ToString().Contains("mutation") ? OperationType.Mutation : OperationType.Query).ToString(),
60+
httpURL: string.Empty, // GraphQL has no route
61+
queryString: null, // GraphQL has no query-string
62+
userRole: httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].FirstOrDefault() ?? httpContext.User.FindFirst("role")?.Value,
63+
apiType: apiType);
64+
}
65+
}
66+
67+
await InvokeAsync();
68+
}
69+
finally
70+
{
71+
stopwatch.Stop();
72+
73+
HttpStatusCode statusCode;
74+
75+
// We want to ignore introspection queries DAB uses to check access to GraphQL since they are not sent by the user.
76+
if (!isIntrospectionQuery)
77+
{
78+
// There is an error in GraphQL when ContextData is not null
79+
if (context.Result!.ContextData is not null)
80+
{
81+
if (context.Result.ContextData.ContainsKey(WellKnownContextData.ValidationErrors))
82+
{
83+
statusCode = HttpStatusCode.BadRequest;
84+
}
85+
else if (context.Result.ContextData.ContainsKey(WellKnownContextData.OperationNotAllowed))
86+
{
87+
statusCode = HttpStatusCode.MethodNotAllowed;
88+
}
89+
else
90+
{
91+
statusCode = HttpStatusCode.InternalServerError;
92+
}
93+
94+
Exception ex = new();
95+
if (context.Result.Errors is not null)
96+
{
97+
string errorMessage = context.Result.Errors[0].Message;
98+
ex = new(errorMessage);
99+
}
100+
101+
// Activity will track error
102+
activity?.TrackMainControllerActivityFinishedWithException(ex, statusCode);
103+
TelemetryMetricsHelper.TrackError(method, statusCode, route, apiType, ex);
104+
}
105+
else
106+
{
107+
statusCode = HttpStatusCode.OK;
108+
activity?.TrackMainControllerActivityFinished(statusCode);
109+
}
110+
111+
TelemetryMetricsHelper.TrackRequest(method, statusCode, route, apiType);
112+
TelemetryMetricsHelper.TrackRequestDuration(method, statusCode, route, apiType, stopwatch.Elapsed);
113+
TelemetryMetricsHelper.DecrementActiveRequests(apiType);
114+
}
35115
}
36116

37-
await _next(context).ConfigureAwait(false);
117+
async Task InvokeAsync()
118+
{
119+
if (context.ContextData.TryGetValue(nameof(HttpContext), out object? value) &&
120+
value is HttpContext httpContext)
121+
{
122+
// Because Request.Headers is a NameValueCollection type, key not found will return StringValues.Empty and not an exception.
123+
StringValues clientRoleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER];
124+
context.ContextData.TryAdd(key: AuthorizationResolver.CLIENT_ROLE_HEADER, value: clientRoleHeader);
125+
}
126+
127+
await _next(context).ConfigureAwait(false);
128+
}
38129
}
39130
}
131+

src/Core/Services/ExecutionHelper.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Diagnostics;
45
using System.Globalization;
56
using System.Net;
67
using System.Text.Json;
@@ -9,6 +10,7 @@
910
using Azure.DataApiBuilder.Core.Models;
1011
using Azure.DataApiBuilder.Core.Resolvers;
1112
using Azure.DataApiBuilder.Core.Resolvers.Factories;
13+
using Azure.DataApiBuilder.Core.Telemetry;
1214
using Azure.DataApiBuilder.Service.Exceptions;
1315
using Azure.DataApiBuilder.Service.GraphQLBuilder;
1416
using Azure.DataApiBuilder.Service.GraphQLBuilder.CustomScalars;
@@ -19,6 +21,7 @@
1921
using HotChocolate.Resolvers;
2022
using HotChocolate.Types.NodaTime;
2123
using NodaTime.Text;
24+
using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
2225

2326
namespace Azure.DataApiBuilder.Service.Services
2427
{
@@ -52,6 +55,8 @@ public ExecutionHelper(
5255
/// </param>
5356
public async ValueTask ExecuteQueryAsync(IMiddlewareContext context)
5457
{
58+
using Activity? activity = StartQueryActivity(context);
59+
5560
string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig());
5661
DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName);
5762
IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType);
@@ -93,6 +98,8 @@ public async ValueTask ExecuteQueryAsync(IMiddlewareContext context)
9398
/// </param>
9499
public async ValueTask ExecuteMutateAsync(IMiddlewareContext context)
95100
{
101+
using Activity? activity = StartQueryActivity(context);
102+
96103
string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig());
97104
DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName);
98105
IQueryEngine queryEngine = _queryEngineFactory.GetQueryEngine(ds.DatabaseType);
@@ -129,6 +136,31 @@ public async ValueTask ExecuteMutateAsync(IMiddlewareContext context)
129136
}
130137
}
131138

139+
/// <summary>
140+
/// Starts the activity for the query
141+
/// </summary>
142+
/// <param name="context">
143+
/// The middleware context.
144+
/// </param>
145+
private Activity? StartQueryActivity(IMiddlewareContext context)
146+
{
147+
string route = _runtimeConfigProvider.GetConfig().GraphQLPath.Trim('/');
148+
Kestral method = Kestral.Post;
149+
150+
Activity? activity = TelemetryTracesHelper.DABActivitySource.StartActivity($"{method} /{route}");
151+
152+
if (activity is not null)
153+
{
154+
string dataSourceName = GraphQLUtils.GetDataSourceNameFromGraphQLContext(context, _runtimeConfigProvider.GetConfig());
155+
DataSource ds = _runtimeConfigProvider.GetConfig().GetDataSourceFromDataSourceName(dataSourceName);
156+
activity.TrackQueryActivityStarted(
157+
databaseType: ds.DatabaseType,
158+
dataSourceName: dataSourceName);
159+
}
160+
161+
return activity;
162+
}
163+
132164
/// <summary>
133165
/// Represents a pure resolver for a leaf field.
134166
/// This resolver extracts the field value from the json object.
@@ -441,7 +473,7 @@ internal static IType InnerMostType(IType type)
441473

442474
public static InputObjectType InputObjectTypeFromIInputField(IInputField field)
443475
{
444-
return (InputObjectType)(InnerMostType(field.Type));
476+
return (InputObjectType)InnerMostType(field.Type);
445477
}
446478

447479
/// <summary>

src/Service/Telemetry/TelemetryMetricsHelper.cs renamed to src/Core/Telemetry/TelemetryMetricsHelper.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System;
5-
using System.Collections.Generic;
64
using System.Diagnostics.Metrics;
75
using System.Net;
86
using Azure.DataApiBuilder.Config.ObjectModel;
97
using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
108

11-
namespace Azure.DataApiBuilder.Service.Telemetry
9+
namespace Azure.DataApiBuilder.Core.Telemetry
1210
{
1311
/// <summary>
1412
/// Helper class for tracking telemetry metrics such as active requests, errors, total requests,

src/Service/Telemetry/TelemetryTracesHelper.cs renamed to src/Core/Telemetry/TelemetryTracesHelper.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System;
54
using System.Diagnostics;
65
using System.Net;
76
using Azure.DataApiBuilder.Config.ObjectModel;
87
using OpenTelemetry.Trace;
98
using Kestral = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpMethod;
109

11-
namespace Azure.DataApiBuilder.Service.Telemetry
10+
namespace Azure.DataApiBuilder.Core.Telemetry
1211
{
1312
public static class TelemetryTracesHelper
1413
{
@@ -18,7 +17,7 @@ public static class TelemetryTracesHelper
1817
public static readonly ActivitySource DABActivitySource = new("DataApiBuilder");
1918

2019
/// <summary>
21-
/// Tracks the start of a REST controller activity.
20+
/// Tracks the start of the main controller activity.
2221
/// </summary>
2322
/// <param name="activity">The activity instance.</param>
2423
/// <param name="httpMethod">The HTTP method of the request (e.g., GET, POST).</param>
@@ -28,11 +27,11 @@ public static class TelemetryTracesHelper
2827
/// <param name="queryString">The query string of the request, if any.</param>
2928
/// <param name="userRole">The role of the user making the request.</param>
3029
/// <param name="apiType">The type of API being used (e.g., REST, GraphQL).</param>
31-
public static void TrackRestControllerActivityStarted(
30+
public static void TrackMainControllerActivityStarted(
3231
this Activity activity,
3332
Kestral httpMethod,
3433
string userAgent,
35-
string actionType,
34+
string actionType, // CRUD(EntityActionOperation) for REST, Query|Mutation(OperationType) for GraphQL
3635
string httpURL,
3736
string? queryString,
3837
string? userRole,
@@ -78,11 +77,11 @@ public static void TrackQueryActivityStarted(
7877
}
7978

8079
/// <summary>
81-
/// Tracks the completion of a REST controller activity.
80+
/// Tracks the completion of the main controller activity without any exceptions.
8281
/// </summary>
8382
/// <param name="activity">The activity instance.</param>
8483
/// <param name="statusCode">The HTTP status code of the response.</param>
85-
public static void TrackRestControllerActivityFinished(
84+
public static void TrackMainControllerActivityFinished(
8685
this Activity activity,
8786
HttpStatusCode statusCode)
8887
{
@@ -93,12 +92,12 @@ public static void TrackRestControllerActivityFinished(
9392
}
9493

9594
/// <summary>
96-
/// Tracks the completion of a REST controller activity with an exception.
95+
/// Tracks the completion of the main controller activity with an exception.
9796
/// </summary>
9897
/// <param name="activity">The activity instance.</param>
9998
/// <param name="ex">The exception that occurred.</param>
10099
/// <param name="statusCode">The HTTP status code of the response.</param>
101-
public static void TrackRestControllerActivityFinishedWithException(
100+
public static void TrackMainControllerActivityFinishedWithException(
102101
this Activity activity,
103102
Exception ex,
104103
HttpStatusCode statusCode)

src/Service/Controllers/RestController.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
using Azure.DataApiBuilder.Core.Configurations;
1212
using Azure.DataApiBuilder.Core.Models;
1313
using Azure.DataApiBuilder.Core.Services;
14+
using Azure.DataApiBuilder.Core.Telemetry;
1415
using Azure.DataApiBuilder.Service.Exceptions;
15-
using Azure.DataApiBuilder.Service.Telemetry;
1616
using Microsoft.AspNetCore.Http;
1717
using Microsoft.AspNetCore.Mvc;
1818
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@@ -208,7 +208,7 @@ private async Task<IActionResult> HandleOperation(
208208

209209
if (activity is not null)
210210
{
211-
activity.TrackRestControllerActivityStarted(
211+
activity.TrackMainControllerActivityStarted(
212212
Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true),
213213
HttpContext.Request.Headers["User-Agent"].ToString(),
214214
operationType.ToString(),
@@ -261,7 +261,7 @@ private async Task<IActionResult> HandleOperation(
261261
if (activity is not null && activity.IsAllDataRequested)
262262
{
263263
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(statusCode.ToString(), ignoreCase: true);
264-
activity.TrackRestControllerActivityFinished(httpStatusCode);
264+
activity.TrackMainControllerActivityFinished(httpStatusCode);
265265
}
266266

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

276276
Response.StatusCode = (int)ex.StatusCode;
277-
activity?.TrackRestControllerActivityFinishedWithException(ex, ex.StatusCode);
277+
activity?.TrackMainControllerActivityFinishedWithException(ex, ex.StatusCode);
278278

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

292292
HttpMethod method = Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true);
293-
activity?.TrackRestControllerActivityFinishedWithException(ex, HttpStatusCode.InternalServerError);
293+
activity?.TrackMainControllerActivityFinishedWithException(ex, HttpStatusCode.InternalServerError);
294294

295295
TelemetryMetricsHelper.TrackError(method, HttpStatusCode.InternalServerError, route, ApiType.REST, ex);
296296
return ErrorResponse(

src/Service/Startup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using Azure.DataApiBuilder.Core.Services.Cache;
2424
using Azure.DataApiBuilder.Core.Services.MetadataProviders;
2525
using Azure.DataApiBuilder.Core.Services.OpenAPI;
26+
using Azure.DataApiBuilder.Core.Telemetry;
2627
using Azure.DataApiBuilder.Service.Controllers;
2728
using Azure.DataApiBuilder.Service.Exceptions;
2829
using Azure.DataApiBuilder.Service.HealthCheck;

0 commit comments

Comments
 (0)