11using System . Diagnostics ;
2+ using System . Net . Http . Headers ;
23using System . Text . Json ;
4+ using System . Text . RegularExpressions ;
35using EventStore . Client ;
46using KurrentDB . Client ;
57using HealthMonitoring . Domain ;
8+ using Microsoft . Extensions . Logging ;
69
710namespace HealthMonitoring . Monitoring ;
811
@@ -11,6 +14,41 @@ public interface IKurrentDbProbe
1114 Task ProbeAsync ( MonitoredService service , CancellationToken cancellationToken ) ;
1215}
1316
17+ public interface IKurrentDbVersionProbe
18+ {
19+ Task < string ? > GetVersionAsync ( MonitoredService service , CancellationToken cancellationToken ) ;
20+ }
21+
22+ public static partial class KurrentDbVersionFormatter
23+ {
24+ public static string ? Format ( string ? rawVersion )
25+ {
26+ if ( string . IsNullOrWhiteSpace ( rawVersion ) ) return null ;
27+
28+ var match = VersionRegex ( ) . Match ( rawVersion . Trim ( ) ) ;
29+ return match . Success ? $ "KurrentDB { match . Value } " : null ;
30+ }
31+
32+ [ GeneratedRegex ( @"\b\d+(?:\.\d+){1,2}\b" ) ]
33+ private static partial Regex VersionRegex ( ) ;
34+ }
35+
36+ public sealed class KurrentDbRegistrationVersionResolver ( IKurrentDbVersionProbe probe , ILogger < KurrentDbRegistrationVersionResolver > logger )
37+ {
38+ public async Task < string ? > ResolveAsync ( MonitoredService service , string ? fallback , CancellationToken cancellationToken )
39+ {
40+ try
41+ {
42+ return KurrentDbVersionFormatter . Format ( await probe . GetVersionAsync ( service , cancellationToken ) ) ?? fallback ;
43+ }
44+ catch ( Exception exception ) when ( ! cancellationToken . IsCancellationRequested )
45+ {
46+ logger . LogWarning ( exception , "Unable to determine KurrentDB version while registering a service." ) ;
47+ return fallback ;
48+ }
49+ }
50+ }
51+
1452public sealed class KurrentDbMonitorClient ( IKurrentDbProbe probe ) : IHealthEndpointClient
1553{
1654 public async Task < HealthEndpointResult > CheckAsync ( MonitoredService service , CancellationToken cancellationToken )
@@ -71,3 +109,63 @@ public async Task ProbeAsync(MonitoredService service, CancellationToken cancell
71109 }
72110 }
73111}
112+
113+ public sealed class KurrentDbVersionProbe ( IHttpClientFactory httpClientFactory ) : IKurrentDbVersionProbe
114+ {
115+ public async Task < string ? > GetVersionAsync ( MonitoredService service , CancellationToken cancellationToken )
116+ {
117+ if ( string . IsNullOrWhiteSpace ( service . ConnectionString ) )
118+ throw new InvalidOperationException ( "KurrentDB connection string is not configured." ) ;
119+
120+ var connectionUri = new Uri ( service . ConnectionString , UriKind . Absolute ) ;
121+ if ( connectionUri . Scheme . EndsWith ( "+discover" , StringComparison . OrdinalIgnoreCase ) )
122+ throw new InvalidOperationException ( "KurrentDB discovery connection strings cannot be used to query the HTTP info endpoint." ) ;
123+
124+ var useTls = ! string . Equals ( GetQueryValue ( connectionUri , "tls" ) , "false" , StringComparison . OrdinalIgnoreCase ) ;
125+ var infoUri = new UriBuilder ( connectionUri )
126+ {
127+ Scheme = useTls ? Uri . UriSchemeHttps : Uri . UriSchemeHttp ,
128+ Port = connectionUri . IsDefaultPort ? ( useTls ? 443 : 80 ) : connectionUri . Port ,
129+ Path = "/info" ,
130+ Query = string . Empty ,
131+ UserName = string . Empty ,
132+ Password = string . Empty
133+ } . Uri ;
134+
135+ using var timeoutSource = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
136+ timeoutSource . CancelAfter ( service . RequestTimeout ) ;
137+ using var request = new HttpRequestMessage ( HttpMethod . Get , infoUri ) ;
138+ if ( ! string . IsNullOrEmpty ( connectionUri . UserInfo ) )
139+ {
140+ var separator = connectionUri . UserInfo . IndexOf ( ':' ) ;
141+ var userName = separator < 0 ? connectionUri . UserInfo : connectionUri . UserInfo [ .. separator ] ;
142+ var password = separator < 0 ? string . Empty : connectionUri . UserInfo [ ( separator + 1 ) .. ] ;
143+ var credentials = $ "{ Uri . UnescapeDataString ( userName ) } :{ Uri . UnescapeDataString ( password ) } ";
144+ request . Headers . Authorization = new AuthenticationHeaderValue ( "Basic" , Convert . ToBase64String ( System . Text . Encoding . UTF8 . GetBytes ( credentials ) ) ) ;
145+ }
146+
147+ using var response = await httpClientFactory . CreateClient ( ) . SendAsync ( request , timeoutSource . Token ) ;
148+ response . EnsureSuccessStatusCode ( ) ;
149+ using var document = await JsonDocument . ParseAsync ( await response . Content . ReadAsStreamAsync ( timeoutSource . Token ) , cancellationToken : timeoutSource . Token ) ;
150+
151+ foreach ( var propertyName in new [ ] { "esVersion" , "version" , "serverVersion" } )
152+ {
153+ if ( document . RootElement . TryGetProperty ( propertyName , out var property ) && property . ValueKind == JsonValueKind . String )
154+ return property . GetString ( ) ;
155+ }
156+
157+ return null ;
158+ }
159+
160+ private static string ? GetQueryValue ( Uri uri , string name )
161+ {
162+ foreach ( var part in uri . Query . TrimStart ( '?' ) . Split ( '&' , StringSplitOptions . RemoveEmptyEntries ) )
163+ {
164+ var values = part . Split ( '=' , 2 ) ;
165+ if ( values . Length == 2 && string . Equals ( Uri . UnescapeDataString ( values [ 0 ] ) , name , StringComparison . OrdinalIgnoreCase ) )
166+ return Uri . UnescapeDataString ( values [ 1 ] ) ;
167+ }
168+
169+ return null ;
170+ }
171+ }
0 commit comments