Skip to content

Commit 4e95b7b

Browse files
authored
Enhance DataEngine health checks to use component health endpoints with fallback (#99)
1 parent 0330eb1 commit 4e95b7b

12 files changed

Lines changed: 608 additions & 49 deletions

File tree

example/docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ services:
3434
- General__CustomerDomainUrl=https://mm-software.com
3535
- Plugins__Instances__0__Name=RelationalDatabasePlugin
3636
- Plugins__Instances__0__baseUrl=http://dpp-plugin:8080
37+
- Plugins__Instances__0__healthEndpoint=/healthz
3738
- Plugins__Instances__0__headerMappings__0__source=Authorization
3839
- Plugins__Instances__0__headerMappings__0__target=X-Auth-Token
3940
- Plugins__Instances__0__headerMappings__0__required=false
@@ -60,26 +61,31 @@ services:
6061
- TemplateManagement__AasTemplateRepository__headerMappings__0__source=Authorization
6162
- TemplateManagement__AasTemplateRepository__headerMappings__0__target=Authorization
6263
- TemplateManagement__AasTemplateRepository__headerMappings__0__required=false
64+
- TemplateManagement__AasTemplateRepository__healthEndpoint=/actuator/health
6365
- TemplateManagement__SubmodelTemplateRepository__Name=SubmodelTemplateRepository
6466
- TemplateManagement__SubmodelTemplateRepository__baseUrl=http://template-repository:8081
6567
- TemplateManagement__SubmodelTemplateRepository__headerMappings__0__source=Authorization
6668
- TemplateManagement__SubmodelTemplateRepository__headerMappings__0__target=Authorization
6769
- TemplateManagement__SubmodelTemplateRepository__headerMappings__0__required=false
70+
- TemplateManagement__SubmodelTemplateRepository__healthEndpoint=/actuator/health
6871
- TemplateManagement__ConceptDescriptionTemplateRepository__Name=ConceptDescriptionTemplateRepository
6972
- TemplateManagement__ConceptDescriptionTemplateRepository__baseUrl=http://template-repository:8081
7073
- TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__source=Authorization
7174
- TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__target=Authorization
7275
- TemplateManagement__ConceptDescriptionTemplateRepository__headerMappings__0__required=false
76+
- TemplateManagement__ConceptDescriptionTemplateRepository__healthEndpoint=/actuator/health
7377
- TemplateManagement__AasTemplateRegistry__Name=AasTemplateRegistry
7478
- TemplateManagement__AasTemplateRegistry__baseUrl=http://aas-template-registry:8080
7579
- TemplateManagement__AasTemplateRegistry__headerMappings__0__source=Authorization
7680
- TemplateManagement__AasTemplateRegistry__headerMappings__0__target=Authorization
7781
- TemplateManagement__AasTemplateRegistry__headerMappings__0__required=false
82+
- TemplateManagement__AasTemplateRegistry__healthEndpoint=/actuator/health
7883
- TemplateManagement__SubmodelTemplateRegistry__Name=SubmodelTemplateRegistry
7984
- TemplateManagement__SubmodelTemplateRegistry__baseUrl=http://sm-template-registry:8080
8085
- TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__source=Authorization
8186
- TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__target=Authorization
8287
- TemplateManagement__SubmodelTemplateRegistry__headerMappings__0__required=false
88+
- TemplateManagement__SubmodelTemplateRegistry__healthEndpoint=/actuator/health
8389
networks:
8490
- twinengine-network
8591

source/AAS.TwinEngine.DataEngine.ModuleTests/TestData/v2-config/appsettings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
{
6666
"Name": "Plugin1",
6767
"baseUrl": "http://localhost:8086",
68+
"healthEndpoint": "/healthz",
6869
"headerMappings": [
6970
{
7071
"source": "Authorization",
@@ -123,6 +124,7 @@
123124
"AasTemplateRepository": {
124125
"Name": "AasTemplateRepository",
125126
"baseUrl": "http://localhost:8081",
127+
"healthEndpoint": "/actuator/health",
126128
"headerMappings": [
127129
{
128130
"source": "Authorization",
@@ -139,16 +141,19 @@
139141
"SubmodelTemplateRepository": {
140142
"Name": "SubmodelTemplateRepository",
141143
"baseUrl": "http://localhost:8081",
144+
"healthEndpoint": "/actuator/health",
142145
"headerMappings": []
143146
},
144147
"ConceptDescriptionTemplateRepository": {
145148
"Name": "ConceptDescriptionTemplateRepository",
146149
"baseUrl": "http://localhost:8081",
150+
"healthEndpoint": "/actuator/health",
147151
"headerMappings": []
148152
},
149153
"AasTemplateRegistry": {
150154
"Name": "AasTemplateRegistry",
151155
"baseUrl": "http://localhost:8082",
156+
"healthEndpoint": "/actuator/health",
152157
"headerMappings": [
153158
{
154159
"source": "Authorization",
@@ -160,6 +165,7 @@
160165
"SubmodelTemplateRegistry": {
161166
"Name": "SubmodelTemplateRegistry",
162167
"baseUrl": "http://localhost:8083",
168+
"healthEndpoint": "/actuator/health",
163169
"headerMappings": [
164170
{
165171
"source": "Authorization",

source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Monitoring/PluginAvailabilityHealthCheckTests.cs

Lines changed: 240 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Net;
1+
using System.Net;
22

33
using AAS.TwinEngine.DataEngine.Infrastructure.Http.Clients;
44
using AAS.TwinEngine.DataEngine.Infrastructure.Monitoring;
@@ -286,6 +286,245 @@ public async Task CheckHealthAsync_Uses_HealthCheck_Client_Names_Without_Retry_P
286286
clientFactory.DidNotReceive().CreateClient($"{HttpClientNames.PluginDataProviderPrefix}Plugin1");
287287
}
288288

289+
[Fact]
290+
public async Task CheckHealthAsync_Uses_Custom_HealthEndpoint_When_Configured()
291+
{
292+
string? requestedPath = null;
293+
294+
var clientFactory = Substitute.For<ICreateClient>();
295+
296+
clientFactory
297+
.CreateClient(Arg.Any<string>())
298+
.Returns(_ =>
299+
{
300+
var handler = new StubHttpMessageHandler((request, _) =>
301+
{
302+
requestedPath = request.RequestUri!.ToString();
303+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
304+
});
305+
306+
return new HttpClient(handler)
307+
{
308+
BaseAddress = new Uri("http://localhost")
309+
};
310+
});
311+
312+
var pluginConfig = Options.Create(new PluginsConfig
313+
{
314+
Instances =
315+
[
316+
new ServiceInstance
317+
{
318+
Name = "Plugin1",
319+
BaseUrl = new Uri("http://localhost"),
320+
HealthEndpoint = "custom-health"
321+
}
322+
]
323+
});
324+
325+
var logger = Substitute.For<ILogger<PluginAvailabilityHealthCheck>>();
326+
327+
var sut = new PluginAvailabilityHealthCheck(clientFactory, pluginConfig, logger);
328+
329+
await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
330+
331+
Assert.Contains("custom-health", requestedPath);
332+
}
333+
334+
[Fact]
335+
public async Task CheckHealthAsync_Uses_Default_HealthEndpoint_When_Not_Configured()
336+
{
337+
string? requestedPath = null;
338+
339+
var clientFactory = Substitute.For<ICreateClient>();
340+
341+
clientFactory
342+
.CreateClient(Arg.Any<string>())
343+
.Returns(_ =>
344+
{
345+
var handler = new StubHttpMessageHandler((request, _) =>
346+
{
347+
requestedPath = request.RequestUri!.ToString();
348+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
349+
});
350+
351+
return new HttpClient(handler)
352+
{
353+
BaseAddress = new Uri("http://localhost")
354+
};
355+
});
356+
357+
var pluginConfig = Options.Create(new PluginsConfig
358+
{
359+
Instances =
360+
[
361+
new ServiceInstance
362+
{
363+
Name = "Plugin1",
364+
BaseUrl = new Uri("http://localhost"),
365+
HealthEndpoint = string.Empty
366+
}
367+
]
368+
});
369+
370+
var logger = Substitute.For<ILogger<PluginAvailabilityHealthCheck>>();
371+
372+
var sut = new PluginAvailabilityHealthCheck(clientFactory, pluginConfig, logger);
373+
374+
await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
375+
376+
Assert.Contains("healthz", requestedPath);
377+
}
378+
379+
[Fact]
380+
public async Task CheckHealthAsync_Uses_Default_When_HealthEndpoint_Is_Empty()
381+
{
382+
string? requestedPath = null;
383+
384+
var clientFactory = Substitute.For<ICreateClient>();
385+
386+
clientFactory
387+
.CreateClient(Arg.Any<string>())
388+
.Returns(_ =>
389+
{
390+
var handler = new StubHttpMessageHandler((request, _) =>
391+
{
392+
requestedPath = request.RequestUri!.ToString();
393+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
394+
});
395+
396+
return new HttpClient(handler)
397+
{
398+
BaseAddress = new Uri("http://localhost")
399+
};
400+
});
401+
402+
var pluginConfig = Options.Create(new PluginsConfig
403+
{
404+
Instances =
405+
[
406+
new ServiceInstance
407+
{
408+
Name = "Plugin1",
409+
BaseUrl = new Uri("http://localhost"),
410+
HealthEndpoint = ""
411+
}
412+
]
413+
});
414+
415+
var logger = Substitute.For<ILogger<PluginAvailabilityHealthCheck>>();
416+
417+
var sut = new PluginAvailabilityHealthCheck(clientFactory, pluginConfig, logger);
418+
419+
await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
420+
421+
Assert.Contains("healthz", requestedPath);
422+
}
423+
424+
[Fact]
425+
public async Task CheckHealthAsync_LogsWarning_When_HealthEndpoint_Is_Blank()
426+
{
427+
var clientFactory = Substitute.For<ICreateClient>();
428+
429+
clientFactory
430+
.CreateClient(Arg.Any<string>())
431+
.Returns(_ =>
432+
{
433+
var handler = new StubHttpMessageHandler((_, _) =>
434+
Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)));
435+
436+
return new HttpClient(handler)
437+
{
438+
BaseAddress = new Uri("http://localhost")
439+
};
440+
});
441+
442+
var pluginConfig = Options.Create(new PluginsConfig
443+
{
444+
Instances =
445+
[
446+
new ServiceInstance
447+
{
448+
Name = "Plugin1",
449+
BaseUrl = new Uri("http://localhost"),
450+
HealthEndpoint = ""
451+
}
452+
]
453+
});
454+
455+
var logger = Substitute.For<ILogger<PluginAvailabilityHealthCheck>>();
456+
457+
var sut = new PluginAvailabilityHealthCheck(clientFactory, pluginConfig, logger);
458+
459+
await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
460+
461+
logger.Received(1).Log(
462+
LogLevel.Warning,
463+
Arg.Any<EventId>(),
464+
Arg.Any<object>(),
465+
Arg.Any<Exception>(),
466+
Arg.Any<Func<object, Exception, string>>());
467+
}
468+
469+
[Fact]
470+
public async Task CheckHealthAsync_WhenMultiplePlugins_OneUsesCustomAndOneUsesDefaultEndpoint()
471+
{
472+
// Arrange
473+
var requestedPaths = new List<string>();
474+
475+
var clientFactory = Substitute.For<ICreateClient>();
476+
477+
clientFactory
478+
.CreateClient(Arg.Any<string>())
479+
.Returns(_ =>
480+
{
481+
var handler = new StubHttpMessageHandler((request, _) =>
482+
{
483+
requestedPaths.Add(request.RequestUri!.ToString());
484+
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
485+
});
486+
487+
return new HttpClient(handler)
488+
{
489+
BaseAddress = new Uri("http://localhost")
490+
};
491+
});
492+
493+
var pluginConfig = Options.Create(new PluginsConfig
494+
{
495+
Instances =
496+
[
497+
new ServiceInstance
498+
{
499+
Name = "Plugin1",
500+
BaseUrl = new Uri("http://localhost"),
501+
HealthEndpoint = "custom-health"
502+
},
503+
new ServiceInstance
504+
{
505+
Name = "Plugin2",
506+
BaseUrl = new Uri("http://localhost"),
507+
HealthEndpoint = null
508+
}
509+
]
510+
});
511+
512+
var logger = Substitute.For<ILogger<PluginAvailabilityHealthCheck>>();
513+
514+
var sut = new PluginAvailabilityHealthCheck(clientFactory, pluginConfig, logger);
515+
516+
// Act
517+
var result = await sut.CheckHealthAsync(new HealthCheckContext(), CancellationToken.None);
518+
519+
// Assert
520+
Assert.Equal(HealthStatus.Healthy, result.Status);
521+
522+
Assert.Equal(2, requestedPaths.Count);
523+
524+
Assert.Contains(requestedPaths, p => p.Contains("custom-health"));
525+
Assert.Contains(requestedPaths, p => p.Contains("healthz"));
526+
}
527+
289528
private static HttpClient CreateHttpClient(HttpStatusCode statusCode)
290529
{
291530
var handler = new StubHttpMessageHandler((_, _) =>

0 commit comments

Comments
 (0)