Skip to content

2554 Enhance REST OTEL instrumentation with custom metrics and traces #2617

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
343e62c
initialize custom metrics/traces for REST endpoint
tommasodotNET Feb 27, 2025
f807727
initialize custom traces on sql db
tommasodotNET Feb 27, 2025
ef47e17
traces refactor
tommasodotNET Mar 12, 2025
6b2d1f7
fixes otel logging
tommasodotNET Mar 14, 2025
970360c
fixes otel logging resource name
tommasodotNET Mar 14, 2025
2199d68
cleans up pr
tommasodotNET Mar 14, 2025
0176cfd
Update src/Service/Controllers/RestController.cs
tommasodotNET Mar 17, 2025
1393def
Update src/Service/Controllers/RestController.cs
tommasodotNET Mar 17, 2025
5db6e92
adding null check on route
tommasodotNET Mar 31, 2025
5a4848b
Merge branch 'features/2554-enh-otel' of github.com:tommasodotNET/dat…
tommasodotNET Mar 31, 2025
fc44a21
Update src/Service/Controllers/RestController.cs
tommasodotNET Apr 3, 2025
e5ff447
Update src/Service/Controllers/RestController.cs
tommasodotNET Apr 3, 2025
1bb648a
Update src/Service/Telemetry/TelemetryTracesHelper.cs
tommasodotNET Apr 3, 2025
eb23e32
Update src/Service/Telemetry/TelemetryTracesHelper.cs
tommasodotNET Apr 3, 2025
0d07c55
Update src/Service/Telemetry/TelemetryTracesHelper.cs
tommasodotNET Apr 3, 2025
5859684
Update src/Service/Telemetry/TelemetryTracesHelper.cs
tommasodotNET Apr 3, 2025
a87f54d
adds docs in TelemetryMetricsHelper
tommasodotNET Apr 3, 2025
4c3e8e9
adds doc on TelemetryTracesHelper
tommasodotNET Apr 3, 2025
ebea19d
adds check queryString is not null
tommasodotNET Apr 3, 2025
2e3f011
adds comments on activities
tommasodotNET Apr 3, 2025
96deeaf
removes unnecessary usings in program.cs
tommasodotNET Apr 3, 2025
0dd2a92
fixes missing meter name
tommasodotNET Apr 3, 2025
3e80105
uses updowncounter for active requests
tommasodotNET Apr 3, 2025
7ff76ce
removes check on route
tommasodotNET Apr 3, 2025
9bc5822
Update src/Service/Telemetry/TelemetryTracesHelper.cs
tommasodotNET Apr 3, 2025
0cad4a8
Update src/Service/Controllers/RestController.cs
tommasodotNET Apr 3, 2025
ba993e3
Merge branch 'main' into features/2554-enh-otel
RubenCerna2079 Apr 3, 2025
1f66f8f
Merge branch 'main' into features/2554-enh-otel
RubenCerna2079 Apr 4, 2025
c6414b6
checks route split
tommasodotNET Apr 5, 2025
8e5d481
Merge branch 'features/2554-enh-otel' of github.com:tommasodotNET/dat…
tommasodotNET Apr 5, 2025
30c61ca
removes activity disposal
tommasodotNET Apr 8, 2025
045e5fa
fixes typo on request finished with exception tracking
tommasodotNET Apr 10, 2025
f8a4b20
Update src/Service/Controllers/RestController.cs
tommasodotNET Apr 11, 2025
18dd1f3
handle userRole with X-MS-API-ROLE and add check for nullability
tommasodotNET Apr 11, 2025
d0065c8
Merge branch 'features/2554-enh-otel' of github.com:tommasodotNET/dat…
tommasodotNET Apr 11, 2025
d2a9cff
fixes logs
tommasodotNET Apr 11, 2025
cf1b821
removes commented otel logs
tommasodotNET Apr 13, 2025
b5d3f7b
adds asp net core base logging
tommasodotNET Apr 14, 2025
e0f06a8
Merge branch 'main' into features/2554-enh-otel
aaronburtle Apr 16, 2025
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
2 changes: 2 additions & 0 deletions src/Core/Resolvers/SqlQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMeta
JsonElement result = await _cache.GetOrSetAsync<JsonElement>(queryExecutor, queryMetadata, cacheEntryTtl: runtimeConfig.GetEntityCacheEntryTtl(entityName: structure.EntityName));
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(result);
JsonDocument cacheServiceResponse = JsonDocument.Parse(jsonBytes);

return cacheServiceResponse;
}
}
Expand All @@ -348,6 +349,7 @@ public object ResolveList(JsonElement array, IObjectField fieldSchema, ref IMeta
httpContext: _httpContextAccessor.HttpContext!,
args: null,
dataSourceName: dataSourceName);

return response;
}

Expand Down
1 change: 0 additions & 1 deletion src/Service.Tests/Configuration/OpenTelemetryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public void CleanUpTelemetryConfig()
File.Delete(CONFIG_WITHOUT_TELEMETRY);
}

Startup.OpenTelemetryOptions = new();
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Service/Azure.DataApiBuilder.Service.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
Expand Down Expand Up @@ -77,7 +77,7 @@
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
Expand Down
67 changes: 66 additions & 1 deletion src/Service/Controllers/RestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Core.Configurations;
using Azure.DataApiBuilder.Core.Models;
using Azure.DataApiBuilder.Core.Services;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Telemetry;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -47,11 +50,14 @@ public class RestController : ControllerBase

private readonly ILogger<RestController> _logger;

private readonly RuntimeConfigProvider _runtimeConfigProvider;

/// <summary>
/// Constructor.
/// </summary>
public RestController(RestService restService, IOpenApiDocumentor openApiDocumentor, ILogger<RestController> logger)
public RestController(RuntimeConfigProvider runtimeConfigProvider, RestService restService, IOpenApiDocumentor openApiDocumentor, ILogger<RestController> logger)
{
_runtimeConfigProvider = runtimeConfigProvider;
_restService = restService;
_openApiDocumentor = openApiDocumentor;
_logger = logger;
Expand Down Expand Up @@ -185,8 +191,25 @@ private async Task<IActionResult> HandleOperation(
string route,
EntityActionOperation operationType)
{
Stopwatch stopwatch = Stopwatch.StartNew();
// This activity tracks the entire REST request.
using Activity? activity = TelemetryTracesHelper.DABActivitySource.StartActivity($"{HttpContext.Request.Method} {(route.Split('/').Length > 1 ? route.Split('/')[1] : "")}");
if (activity is not null)
{
activity.TrackRestControllerActivityStarted(
HttpContext.Request.Method,
HttpContext.Request.Headers["User-Agent"].ToString(),
operationType.ToString(),
route,
HttpContext.Request.QueryString.ToString(),
HttpContext.User.FindFirst("role")?.Value,
"REST");
}

TelemetryMetricsHelper.IncrementActiveRequests();
try
{

if (route.Equals(REDIRECTED_ROUTE))
{
return NotFound();
Expand All @@ -208,8 +231,26 @@ private async Task<IActionResult> HandleOperation(

(string entityName, string primaryKeyRoute) = _restService.GetEntityNameAndPrimaryKeyRouteFromRoute(routeAfterPathBase);

// This activity tracks the query execution. This will create a new activity nested under the REST request activity.
using Activity? queryActivity = TelemetryTracesHelper.DABActivitySource.StartActivity($"QUERY {entityName}");
IActionResult? result = await _restService.ExecuteAsync(entityName, operationType, primaryKeyRoute);

RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
string dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName);
DatabaseType databaseType = runtimeConfig.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType;

if (queryActivity is not null)
{
queryActivity.TrackQueryActivityStarted(
databaseType.ToString(),
dataSourceName);
}

if (queryActivity is not null && queryActivity.IsAllDataRequested)
{
queryActivity.Dispose();
}

if (result is null)
{
throw new DataApiBuilderException(
Expand All @@ -218,6 +259,13 @@ private async Task<IActionResult> HandleOperation(
subStatusCode: DataApiBuilderException.SubStatusCodes.EntityNotFound);
}

int statusCode = (result as ObjectResult)?.StatusCode ?? (result as StatusCodeResult)?.StatusCode ?? (result as JsonResult)?.StatusCode ?? 200;
if (activity is not null && activity.IsAllDataRequested)
{
activity.TrackRestControllerActivityFinished(statusCode);
}

TelemetryMetricsHelper.TrackRequest(HttpContext.Request.Method, statusCode, route, "REST");
return result;
}
catch (DataApiBuilderException ex)
Expand All @@ -228,6 +276,9 @@ private async Task<IActionResult> HandleOperation(
HttpContextExtensions.GetLoggerCorrelationId(HttpContext));

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

TelemetryMetricsHelper.TrackError(HttpContext.Request.Method, Response.StatusCode, route, "REST", ex);
return ErrorResponse(ex.SubStatusCode.ToString(), ex.Message, ex.StatusCode);
}
catch (Exception ex)
Expand All @@ -238,11 +289,25 @@ private async Task<IActionResult> HandleOperation(
HttpContextExtensions.GetLoggerCorrelationId(HttpContext));

Response.StatusCode = (int)HttpStatusCode.InternalServerError;
activity?.TrackRestControllerActivityFinishedWithWithException(ex, Response.StatusCode);

TelemetryMetricsHelper.TrackError(HttpContext.Request.Method, Response.StatusCode, route, "REST", ex);
return ErrorResponse(
DataApiBuilderException.SubStatusCodes.UnexpectedError.ToString(),
SERVER_ERROR,
HttpStatusCode.InternalServerError);
}
finally
{
stopwatch.Stop();
TelemetryMetricsHelper.TrackRequestDuration(HttpContext.Request.Method, Response.StatusCode, route, "REST", stopwatch.Elapsed.TotalMilliseconds);
if (activity is not null && activity.IsAllDataRequested)
{
activity.Dispose();
}

TelemetryMetricsHelper.DecrementActiveRequests();
}
}

/// <summary>
Expand Down
19 changes: 0 additions & 19 deletions src/Service/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;

namespace Azure.DataApiBuilder.Service
{
Expand Down Expand Up @@ -178,22 +175,6 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele
}
}

if (Startup.OpenTelemetryOptions.Enabled && !string.IsNullOrWhiteSpace(Startup.OpenTelemetryOptions.Endpoint))
{
builder.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(Startup.OpenTelemetryOptions.ServiceName!));
logging.AddOtlpExporter(configure =>
{
configure.Endpoint = new Uri(Startup.OpenTelemetryOptions.Endpoint);
configure.Headers = Startup.OpenTelemetryOptions.Headers;
configure.Protocol = OtlpExportProtocol.Grpc;
});
});
}

builder.AddConsole();
});
}
Expand Down
26 changes: 20 additions & 6 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public class Startup
public static LogLevel MinimumLogLevel = LogLevel.Error;

public static bool IsLogLevelOverriddenByCli;
public static OpenTelemetryOptions OpenTelemetryOptions = new();

public static ApplicationInsightsOptions AppInsightsOptions = new();
public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect";
Expand Down Expand Up @@ -119,31 +118,46 @@ public void ConfigureServices(IServiceCollection services)
&& runtimeConfig?.Runtime?.Telemetry?.OpenTelemetry is not null
&& runtimeConfig.Runtime.Telemetry.OpenTelemetry.Enabled)
{
services.Configure<OpenTelemetryLoggerOptions>(options =>
{
options.IncludeScopes = true;
options.ParseStateValues = true;
options.IncludeFormattedMessage = true;
});
services.AddOpenTelemetry()
.WithLogging(logging =>
{
logging.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!))
.AddOtlpExporter(configure =>
{
configure.Endpoint = new Uri(runtimeConfig.Runtime.Telemetry.OpenTelemetry.Endpoint!);
configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
configure.Protocol = OtlpExportProtocol.Grpc;
});

})
.WithMetrics(metrics =>
{
metrics.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(configure =>
{
configure.Endpoint = new Uri(runtimeConfig.Runtime.Telemetry.OpenTelemetry.Endpoint!);
configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
configure.Protocol = OtlpExportProtocol.Grpc;
})
.AddRuntimeInstrumentation();
.AddMeter(TelemetryMetricsHelper.MeterName);
})
.WithTracing(tracing =>
{
tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(configure =>
{
configure.Endpoint = new Uri(runtimeConfig.Runtime.Telemetry.OpenTelemetry.Endpoint!);
configure.Headers = runtimeConfig.Runtime.Telemetry.OpenTelemetry.Headers;
configure.Protocol = OtlpExportProtocol.Grpc;
});
})
.AddSource(TelemetryTracesHelper.DABActivitySource.Name);
});
}

Expand Down
77 changes: 77 additions & 0 deletions src/Service/Telemetry/TelemetryMetricsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics.Metrics;

namespace Azure.DataApiBuilder.Service.Telemetry
{
/// <summary>
/// Helper class for tracking telemetry metrics such as active requests, errors, total requests,
/// and request durations using the .NET Meter and Counter APIs.
/// </summary>
public static class TelemetryMetricsHelper
{
public static readonly string MeterName = "DataApiBuilder.Metrics";
private static readonly Meter _meter = new(MeterName);
private static readonly UpDownCounter<long> _activeRequests = _meter.CreateUpDownCounter<long>("active_requests");
private static readonly Counter<long> _errorCounter = _meter.CreateCounter<long>("total_errors");
private static readonly Counter<long> _totalRequests = _meter.CreateCounter<long>("total_requests");
private static readonly Histogram<double> _requestDuration = _meter.CreateHistogram<double>("request_duration", "ms");

public static void IncrementActiveRequests() => _activeRequests.Add(1);

public static void DecrementActiveRequests() => _activeRequests.Add(-1);

/// <summary>
/// Tracks a request by incrementing the total requests counter and associating it with metadata.
/// </summary>
/// <param name="method">The HTTP method of the request (e.g., GET, POST).</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <param name="endpoint">The endpoint being accessed.</param>
/// <param name="apiType">The type of API being used (e.g., REST, GraphQL).</param>
public static void TrackRequest(string method, int statusCode, string endpoint, string apiType)
{
_totalRequests.Add(1,
new("method", method),
new("status_code", statusCode),
new("endpoint", endpoint),
new("api_type", apiType));
}

/// <summary>
/// Tracks an error by incrementing the error counter and associating it with metadata.
/// </summary>
/// <param name="method">The HTTP method of the request (e.g., GET, POST).</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <param name="endpoint">The endpoint being accessed.</param>
/// <param name="apiType">The type of API being used (e.g., REST, GraphQL).</param>
/// <param name="ex">The exception that occurred.</param>
public static void TrackError(string method, int statusCode, string endpoint, string apiType, Exception ex)
{
_errorCounter.Add(1,
new("method", method),
new("status_code", statusCode),
new("endpoint", endpoint),
new("api_type", apiType),
new("error_type", ex.GetType().Name));
}

/// <summary>
/// Tracks the duration of a request by recording it in a histogram and associating it with metadata.
/// </summary>
/// <param name="method">The HTTP method of the request (e.g., GET, POST).</param>
/// <param name="statusCode">The HTTP status code of the response.</param>
/// <param name="endpoint">The endpoint being accessed.</param>
/// <param name="apiType">The type of API being used (e.g., REST, GraphQL).</param>
/// <param name="duration">The duration of the request in milliseconds.</param>
public static void TrackRequestDuration(string method, int statusCode, string endpoint, string apiType, double duration)
{
_requestDuration.Record(duration,
new("method", method),
new("status_code", statusCode),
new("endpoint", endpoint),
new("api_type", apiType));
}
}
}
Loading
Loading