Skip to content
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 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
0a21b84
Changes to Tomaso Branch
RubenCerna2079 Apr 21, 2025
3d09aca
Fix Unit Test errors
RubenCerna2079 Apr 21, 2025
d674495
Merge branch 'main' into features/2554-enh-otel
RubenCerna2079 Apr 21, 2025
de36de5
Fixed Unit Test Failure
RubenCerna2079 Apr 21, 2025
be44074
Merge branch 'main' into features/2554-enh-otel
Aniruddh25 Apr 22, 2025
8780c39
Changes based on comments
RubenCerna2079 Apr 23, 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
8 changes: 7 additions & 1 deletion src/Service.Tests/dab-config.CosmosDb_NoSql.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
"provider": "StaticWebApps"
},
"mode": "development"
},
"telemetry": {
"app-insights": {
"enabled": true,
"connection-string": "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://dc.services.visualstudio.com/v2/track"
}
}
},
"entities": {
Expand Down Expand Up @@ -794,4 +800,4 @@
]
}
}
}
}
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
73 changes: 70 additions & 3 deletions src/Service/Controllers/RestController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Linq;
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.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.Extensions.Logging;

namespace Azure.DataApiBuilder.Service.Controllers
Expand Down Expand Up @@ -47,11 +52,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,11 +193,29 @@ private async Task<IActionResult> HandleOperation(
string route,
EntityActionOperation operationType)
{
if (route.Equals(REDIRECTED_ROUTE))
{
return NotFound();
}

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] : string.Empty)}");

try
{
if (route.Equals(REDIRECTED_ROUTE))
TelemetryMetricsHelper.IncrementActiveRequests(ApiType.REST);

if (activity is not null)
{
return NotFound();
activity.TrackRestControllerActivityStarted(
Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true),
HttpContext.Request.Headers["User-Agent"].ToString(),
operationType.ToString(),
route,
HttpContext.Request.QueryString.ToString(),
HttpContext.Request.Headers["X-MS-API-ROLE"].FirstOrDefault() ?? HttpContext.User.FindFirst("role")?.Value,
ApiType.REST);
}

// Validate the PathBase matches the configured REST path.
Expand All @@ -208,8 +234,21 @@ 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,
dataSourceName);
}

if (result is null)
{
throw new DataApiBuilderException(
Expand All @@ -218,6 +257,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)
{
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(statusCode.ToString(), ignoreCase: true);
activity.TrackRestControllerActivityFinished(httpStatusCode);
}

return result;
}
catch (DataApiBuilderException ex)
Expand All @@ -228,6 +274,11 @@ private async Task<IActionResult> HandleOperation(
HttpContextExtensions.GetLoggerCorrelationId(HttpContext));

Response.StatusCode = (int)ex.StatusCode;
activity?.TrackRestControllerActivityFinishedWithException(ex, Enum.Parse<HttpStatusCode>(Response.StatusCode.ToString(), ignoreCase: true));

HttpMethod method = Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true);
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(Response.StatusCode.ToString(), ignoreCase: true);
TelemetryMetricsHelper.TrackError(method, httpStatusCode, route, ApiType.REST, ex);
return ErrorResponse(ex.SubStatusCode.ToString(), ex.Message, ex.StatusCode);
}
catch (Exception ex)
Expand All @@ -238,11 +289,27 @@ private async Task<IActionResult> HandleOperation(
HttpContextExtensions.GetLoggerCorrelationId(HttpContext));

Response.StatusCode = (int)HttpStatusCode.InternalServerError;

HttpMethod method = Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true);
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(Response.StatusCode.ToString(), ignoreCase: true);
activity?.TrackRestControllerActivityFinishedWithException(ex, httpStatusCode);

TelemetryMetricsHelper.TrackError(method, httpStatusCode, route, ApiType.REST, ex);
return ErrorResponse(
DataApiBuilderException.SubStatusCodes.UnexpectedError.ToString(),
SERVER_ERROR,
HttpStatusCode.InternalServerError);
}
finally
{
stopwatch.Stop();
HttpMethod method = Enum.Parse<HttpMethod>(HttpContext.Request.Method, ignoreCase: true);
HttpStatusCode httpStatusCode = Enum.Parse<HttpStatusCode>(Response.StatusCode.ToString(), ignoreCase: true);
TelemetryMetricsHelper.TrackRequest(method, httpStatusCode, route, ApiType.REST);
TelemetryMetricsHelper.TrackRequestDuration(method, httpStatusCode, route, ApiType.REST, stopwatch.Elapsed);

TelemetryMetricsHelper.DecrementActiveRequests(ApiType.REST);
}
}

/// <summary>
Expand Down
59 changes: 53 additions & 6 deletions src/Service/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ public class Startup
public static LogLevel MinimumLogLevel = LogLevel.Error;

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

public static ApplicationInsightsOptions AppInsightsOptions = new();
public static OpenTelemetryOptions OpenTelemetryOptions = new();
public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect";
private HotReloadEventHandler<HotReloadEventArgs> _hotReloadEventHandler = new();
private RuntimeConfigProvider? _configProvider;
Expand Down Expand Up @@ -119,31 +119,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 Expand Up @@ -395,6 +410,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC
{
// Configure Application Insights Telemetry
ConfigureApplicationInsightsTelemetry(app, runtimeConfig);
ConfigureOpenTelemetry(runtimeConfig);

// Config provided before starting the engine.
isRuntimeReady = PerformOnConfigChangeAsync(app).Result;
Expand Down Expand Up @@ -712,6 +728,37 @@ private void ConfigureApplicationInsightsTelemetry(IApplicationBuilder app, Runt
}
}

/// <summary>
/// Configure Open Telemetry based on the loaded runtime configuration. If Open Telemetry
/// is enabled, we can track different events and metrics.
/// </summary>
/// <param name="runtimeConfigurationProvider">The provider used to load runtime configuration.</param>
/// <seealso cref="https://docs.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core#enable-application-insights-telemetry-collection"/>
private void ConfigureOpenTelemetry(RuntimeConfig runtimeConfig)
{
if (runtimeConfig?.Runtime?.Telemetry is not null
&& runtimeConfig.Runtime.Telemetry.OpenTelemetry is not null)
{
OpenTelemetryOptions = runtimeConfig.Runtime.Telemetry.OpenTelemetry;

if (!OpenTelemetryOptions.Enabled)
{
_logger.LogInformation("Open Telemetry are disabled.");
return;
}

if (string.IsNullOrWhiteSpace(OpenTelemetryOptions?.Endpoint))
{
_logger.LogWarning("Logs won't be sent to Open Telemetry because an Open Telemetry connection string is not available in the runtime config.");
return;
}

// Updating Startup Logger to Log from Startup Class.
ILoggerFactory? loggerFactory = Program.GetLoggerFactoryForLogLevel(MinimumLogLevel);
_logger = loggerFactory.CreateLogger<Startup>();
}
}

/// <summary>
/// Sets Static Web Apps EasyAuth as the authentication scheme for the engine.
/// </summary>
Expand Down
81 changes: 81 additions & 0 deletions src/Service/Telemetry/TelemetryMetricsHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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
{
/// <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(ApiType kind) => _activeRequests.Add(1, new KeyValuePair<string, object?>("api_type", kind));

public static void DecrementActiveRequests(ApiType kind) => _activeRequests.Add(-1, new KeyValuePair<string, object?>("api_type", kind));

/// <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(Kestral method, HttpStatusCode statusCode, string endpoint, ApiType 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(Kestral method, HttpStatusCode statusCode, string endpoint, ApiType 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(Kestral method, HttpStatusCode statusCode, string endpoint, ApiType apiType, TimeSpan duration)
{
_requestDuration.Record(duration.TotalMilliseconds,
new("method", method),
new("status_code", statusCode),
new("endpoint", endpoint),
new("api_type", apiType));
}
}
}
Loading