Skip to content

Commit d9c8a29

Browse files
CopilotJerryNixon
andcommitted
Add embedding health check execution and update JSON schema with endpoint/health sub-objects
Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com>
1 parent e8d7238 commit d9c8a29

3 files changed

Lines changed: 171 additions & 2 deletions

File tree

schemas/dab.draft.schema.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,59 @@
686686
"default": 30000,
687687
"minimum": 1,
688688
"maximum": 300000
689+
},
690+
"endpoint": {
691+
"type": "object",
692+
"description": "REST endpoint configuration for the embedding service.",
693+
"additionalProperties": false,
694+
"properties": {
695+
"enabled": {
696+
"type": "boolean",
697+
"description": "Whether the /embed REST endpoint is enabled. Defaults to false.",
698+
"default": false
699+
},
700+
"path": {
701+
"type": "string",
702+
"description": "The endpoint path. Defaults to '/embed'.",
703+
"default": "/embed"
704+
},
705+
"roles": {
706+
"type": "array",
707+
"description": "The roles allowed to access the embedding endpoint. In development mode, defaults to ['anonymous'].",
708+
"items": {
709+
"type": "string"
710+
}
711+
}
712+
}
713+
},
714+
"health": {
715+
"type": "object",
716+
"description": "Health check configuration for the embedding service.",
717+
"additionalProperties": false,
718+
"properties": {
719+
"enabled": {
720+
"type": "boolean",
721+
"description": "Whether health checks are enabled for embeddings. Defaults to true.",
722+
"default": true
723+
},
724+
"threshold-ms": {
725+
"type": "integer",
726+
"description": "The maximum response time in milliseconds to be considered healthy.",
727+
"default": 5000,
728+
"minimum": 1,
729+
"maximum": 300000
730+
},
731+
"test-text": {
732+
"type": "string",
733+
"description": "The text to use for health check validation.",
734+
"default": "health check"
735+
},
736+
"expected-dimensions": {
737+
"type": "integer",
738+
"description": "The expected number of dimensions in the embedding result. If specified, dimension validation is performed.",
739+
"minimum": 1
740+
}
741+
}
689742
}
690743
},
691744
"required": ["provider", "base-url", "api-key"],

src/Service/HealthCheck/HealthCheckHelper.cs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
using System.Threading.Tasks;
1111
using Azure.DataApiBuilder.Config.HealthCheck;
1212
using Azure.DataApiBuilder.Config.ObjectModel;
13+
using Azure.DataApiBuilder.Config.ObjectModel.Embeddings;
1314
using Azure.DataApiBuilder.Core.Authorization;
15+
using Azure.DataApiBuilder.Core.Services.Embeddings;
1416
using Azure.DataApiBuilder.Product;
1517
using Microsoft.AspNetCore.Http;
1618
using Microsoft.Extensions.Logging;
@@ -27,20 +29,24 @@ public class HealthCheckHelper
2729
// Dependencies
2830
private ILogger<HealthCheckHelper> _logger;
2931
private HttpUtilities _httpUtility;
32+
private IEmbeddingService? _embeddingService;
3033
private string _incomingRoleHeader = string.Empty;
3134
private string _incomingRoleToken = string.Empty;
3235

3336
private const string TIME_EXCEEDED_ERROR_MESSAGE = "The threshold for executing the request has exceeded.";
37+
private const string DIMENSIONS_MISMATCH_ERROR_MESSAGE = "The embedding dimensions do not match the expected dimensions.";
3438

3539
/// <summary>
3640
/// Constructor to inject the logger and HttpUtility class.
3741
/// </summary>
3842
/// <param name="logger">Logger to track the log statements.</param>
3943
/// <param name="httpUtility">HttpUtility to call methods from the internal class.</param>
40-
public HealthCheckHelper(ILogger<HealthCheckHelper> logger, HttpUtilities httpUtility)
44+
/// <param name="embeddingService">Optional embedding service for embedding health checks.</param>
45+
public HealthCheckHelper(ILogger<HealthCheckHelper> logger, HttpUtilities httpUtility, IEmbeddingService? embeddingService = null)
4146
{
4247
_logger = logger;
4348
_httpUtility = httpUtility;
49+
_embeddingService = embeddingService;
4450
}
4551

4652
/// <summary>
@@ -159,6 +165,7 @@ private async Task UpdateHealthCheckDetailsAsync(ComprehensiveHealthCheckReport
159165
comprehensiveHealthCheckReport.Checks = new List<HealthCheckResultEntry>();
160166
await UpdateDataSourceHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig);
161167
await UpdateEntityHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig);
168+
await UpdateEmbeddingsHealthCheckResultsAsync(comprehensiveHealthCheckReport, runtimeConfig);
162169
}
163170

164171
// Updates the DataSource Health Check Results in the response.
@@ -351,5 +358,108 @@ private async Task PopulateEntityHealthAsync(ComprehensiveHealthCheckReport comp
351358

352359
return (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage);
353360
}
361+
362+
/// <summary>
363+
/// Updates the Embeddings Health Check Results in the response.
364+
/// Executes a test embedding and validates response time and optionally dimensions.
365+
/// </summary>
366+
private async Task UpdateEmbeddingsHealthCheckResultsAsync(ComprehensiveHealthCheckReport comprehensiveHealthCheckReport, RuntimeConfig runtimeConfig)
367+
{
368+
EmbeddingsOptions? embeddingsOptions = runtimeConfig?.Runtime?.Embeddings;
369+
EmbeddingsHealthCheckConfig? healthConfig = embeddingsOptions?.Health;
370+
371+
// Only run health check if embeddings is enabled, health check is enabled, and embedding service is available
372+
if (embeddingsOptions is null || !embeddingsOptions.Enabled || healthConfig is null || !healthConfig.Enabled || _embeddingService is null)
373+
{
374+
return;
375+
}
376+
377+
if (comprehensiveHealthCheckReport.Checks is null)
378+
{
379+
comprehensiveHealthCheckReport.Checks = new List<HealthCheckResultEntry>();
380+
}
381+
382+
string testText = healthConfig.TestText;
383+
int thresholdMs = healthConfig.ThresholdMs;
384+
int? expectedDimensions = healthConfig.ExpectedDimensions;
385+
386+
try
387+
{
388+
Stopwatch stopwatch = new();
389+
stopwatch.Start();
390+
EmbeddingResult result = await _embeddingService.TryEmbedAsync(testText);
391+
stopwatch.Stop();
392+
393+
int responseTimeMs = (int)stopwatch.ElapsedMilliseconds;
394+
bool isResponseTimeWithinThreshold = responseTimeMs <= thresholdMs;
395+
bool isDimensionsValid = true;
396+
string? errorMessage = null;
397+
398+
if (!result.Success)
399+
{
400+
errorMessage = result.ErrorMessage ?? "Embedding request failed.";
401+
comprehensiveHealthCheckReport.Checks.Add(new HealthCheckResultEntry
402+
{
403+
Name = "embeddings",
404+
ResponseTimeData = new ResponseTimeData
405+
{
406+
ResponseTimeMs = HealthCheckConstants.ERROR_RESPONSE_TIME_MS,
407+
ThresholdMs = thresholdMs
408+
},
409+
Exception = errorMessage,
410+
Tags = new List<string> { HealthCheckConstants.EMBEDDING },
411+
Status = HealthStatus.Unhealthy
412+
});
413+
return;
414+
}
415+
416+
// Validate dimensions if expected dimensions is specified
417+
if (expectedDimensions.HasValue && result.Embedding is not null)
418+
{
419+
isDimensionsValid = result.Embedding.Length == expectedDimensions.Value;
420+
if (!isDimensionsValid)
421+
{
422+
errorMessage = $"{DIMENSIONS_MISMATCH_ERROR_MESSAGE} Expected: {expectedDimensions.Value}, Actual: {result.Embedding.Length}";
423+
}
424+
}
425+
426+
// Check response time threshold
427+
if (!isResponseTimeWithinThreshold)
428+
{
429+
errorMessage = errorMessage is null ? TIME_EXCEEDED_ERROR_MESSAGE : $"{errorMessage} {TIME_EXCEEDED_ERROR_MESSAGE}";
430+
}
431+
432+
bool isHealthy = isResponseTimeWithinThreshold && isDimensionsValid;
433+
434+
comprehensiveHealthCheckReport.Checks.Add(new HealthCheckResultEntry
435+
{
436+
Name = "embeddings",
437+
ResponseTimeData = new ResponseTimeData
438+
{
439+
ResponseTimeMs = responseTimeMs,
440+
ThresholdMs = thresholdMs
441+
},
442+
Exception = errorMessage,
443+
Tags = new List<string> { HealthCheckConstants.EMBEDDING },
444+
Status = isHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy
445+
});
446+
}
447+
catch (Exception ex)
448+
{
449+
_logger.LogError(ex, "Error executing embeddings health check.");
450+
comprehensiveHealthCheckReport.Checks.Add(new HealthCheckResultEntry
451+
{
452+
Name = "embeddings",
453+
ResponseTimeData = new ResponseTimeData
454+
{
455+
ResponseTimeMs = HealthCheckConstants.ERROR_RESPONSE_TIME_MS,
456+
ThresholdMs = thresholdMs
457+
},
458+
Exception = ex.Message,
459+
Tags = new List<string> { HealthCheckConstants.EMBEDDING },
460+
Status = HealthStatus.Unhealthy
461+
});
462+
}
463+
}
354464
}
355465
}

src/Service/Startup.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,13 @@ public void ConfigureServices(IServiceCollection services)
262262
services.AddSingleton<GQLFilterParser>();
263263
services.AddSingleton<RequestValidator>();
264264
services.AddSingleton<RestService>();
265-
services.AddSingleton<HealthCheckHelper>();
265+
services.AddSingleton<HealthCheckHelper>(sp =>
266+
{
267+
ILogger<HealthCheckHelper> logger = sp.GetRequiredService<ILogger<HealthCheckHelper>>();
268+
HttpUtilities httpUtility = sp.GetRequiredService<HttpUtilities>();
269+
IEmbeddingService? embeddingService = sp.GetService<IEmbeddingService>();
270+
return new HealthCheckHelper(logger, httpUtility, embeddingService);
271+
});
266272
services.AddSingleton<HttpUtilities>();
267273
services.AddSingleton<BasicHealthReportResponseWriter>();
268274
services.AddSingleton<ComprehensiveHealthReportResponseWriter>();

0 commit comments

Comments
 (0)