From 3ac9d07e584a029833de826b85b3c9f6b308e6f0 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 1 Oct 2026 08:03:54 +0100 Subject: [PATCH 1/2] fix: package KurrentDB client for support check --- .github/workflows/createrelease.yml | 21 +++++ .github/workflows/pushtomain.yml | 11 +++ .../Invoke-DailySupportCheck.ps1 | 81 ++++--------------- .../KurrentDbClient/KurrentDbClient.csproj | 13 +++ .../KurrentDbClient/KurrentDbEventReader.cs | 58 +++++++++++++ ScheduledTasks/DailySupportCheck/README.md | 4 +- .../Tests/Invoke-DailySupportCheck.Tests.ps1 | 7 ++ .../daily-support-check.json | 2 +- 8 files changed, 129 insertions(+), 68 deletions(-) create mode 100644 ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj create mode 100644 ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbEventReader.cs diff --git a/.github/workflows/createrelease.yml b/.github/workflows/createrelease.yml index f5782a6..4f9aa50 100644 --- a/.github/workflows/createrelease.yml +++ b/.github/workflows/createrelease.yml @@ -41,6 +41,7 @@ jobs: dotnet publish "TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.csproj" --configuration Release --output TransactionProcessing.MerchantPos/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.get_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.get_version.outputs.VERSION }} -p:FileVersion=${{ steps.get_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.get_version.outputs.VERSION }} dotnet publish "TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.csproj" --configuration Release --output TransactionProcessing.MerchantFileProcessor/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.get_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.get_version.outputs.VERSION }} -p:FileVersion=${{ steps.get_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.get_version.outputs.VERSION }} dotnet publish "HealthMonitoring/HealthMonitoring/HealthMonitoring.csproj" --configuration Release --output HealthMonitoring/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.get_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.get_version.outputs.VERSION }} -p:FileVersion=${{ steps.get_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.get_version.outputs.VERSION }} + dotnet publish "ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj" --configuration Release --output ScheduledTasks/DailySupportCheck/KurrentDbClient/publishOutput --no-self-contained - name: Build Release Package (Merchant Pos) run: | @@ -162,6 +163,16 @@ jobs: Where-Object { $_.FullName -notmatch '\\Reports\\|\\Tests\\' } | Copy-Item -Destination $taskScriptDir -Force + $kurrentDbClientOutput = Get-ChildItem -Path $scheduledTasksRoot -Recurse -Directory -Filter publishOutput | + Where-Object { $_.FullName -match '\\DailySupportCheck\\KurrentDbClient\\publishOutput$' } | + Select-Object -First 1 + if (-not $kurrentDbClientOutput) { + throw 'Packaged KurrentDB client output was not found in the scheduled-tasks artifact.' + } + $kurrentDbClientDestination = Join-Path $taskScriptDir 'KurrentDbClient' + New-Item -ItemType Directory -Path $kurrentDbClientDestination -Force | Out-Null + Copy-Item -Path (Join-Path $kurrentDbClientOutput.FullName '*') -Destination $kurrentDbClientDestination -Recurse -Force + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml | Where-Object { $_.FullName -notmatch '\\Tests\\' } | ForEach-Object { @@ -366,6 +377,16 @@ jobs: Where-Object { $_.FullName -notmatch '\\Reports\\|\\Tests\\' } | Copy-Item -Destination $taskScriptDir -Force + $kurrentDbClientOutput = Get-ChildItem -Path $scheduledTasksRoot -Recurse -Directory -Filter publishOutput | + Where-Object { $_.FullName -match '\\DailySupportCheck\\KurrentDbClient\\publishOutput$' } | + Select-Object -First 1 + if (-not $kurrentDbClientOutput) { + throw 'Packaged KurrentDB client output was not found in the scheduled-tasks artifact.' + } + $kurrentDbClientDestination = Join-Path $taskScriptDir 'KurrentDbClient' + New-Item -ItemType Directory -Path $kurrentDbClientDestination -Force | Out-Null + Copy-Item -Path (Join-Path $kurrentDbClientOutput.FullName '*') -Destination $kurrentDbClientDestination -Recurse -Force + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml | Where-Object { $_.FullName -notmatch '\\Tests\\' } | ForEach-Object { diff --git a/.github/workflows/pushtomain.yml b/.github/workflows/pushtomain.yml index a777749..18db2a8 100644 --- a/.github/workflows/pushtomain.yml +++ b/.github/workflows/pushtomain.yml @@ -56,6 +56,7 @@ jobs: dotnet publish "TransactionProcessing.MerchantPos/TransactionProcessing.MerchantPos.csproj" --configuration Release --output TransactionProcessing.MerchantPos/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.next_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.next_version.outputs.VERSION }} -p:FileVersion=${{ steps.next_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.next_version.outputs.VERSION }} dotnet publish "TransactionProcessing.MerchantFileProcessor/TransactionProcessing.MerchantFileProcessor.csproj" --configuration Release --output TransactionProcessing.MerchantFileProcessor/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.next_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.next_version.outputs.VERSION }} -p:FileVersion=${{ steps.next_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.next_version.outputs.VERSION }} dotnet publish "HealthMonitoring/HealthMonitoring/HealthMonitoring.csproj" --configuration Release --output HealthMonitoring/publishOutput -r win-x64 --self-contained -p:Version=${{ steps.next_version.outputs.VERSION }} -p:AssemblyVersion=${{ steps.next_version.outputs.VERSION }} -p:FileVersion=${{ steps.next_version.outputs.VERSION }} -p:InformationalVersion=${{ steps.next_version.outputs.VERSION }} + dotnet publish "ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj" --configuration Release --output ScheduledTasks/DailySupportCheck/KurrentDbClient/publishOutput --no-self-contained - name: Build Release Package (Merchant Pos) run: | @@ -176,6 +177,16 @@ jobs: Where-Object { $_.FullName -notmatch '\\Reports\\|\\Tests\\' } | Copy-Item -Destination $taskScriptDir -Force + $kurrentDbClientOutput = Get-ChildItem -Path $scheduledTasksRoot -Recurse -Directory -Filter publishOutput | + Where-Object { $_.FullName -match '\\DailySupportCheck\\KurrentDbClient\\publishOutput$' } | + Select-Object -First 1 + if (-not $kurrentDbClientOutput) { + throw 'Packaged KurrentDB client output was not found in the scheduled-tasks artifact.' + } + $kurrentDbClientDestination = Join-Path $taskScriptDir 'KurrentDbClient' + New-Item -ItemType Directory -Path $kurrentDbClientDestination -Force | Out-Null + Copy-Item -Path (Join-Path $kurrentDbClientOutput.FullName '*') -Destination $kurrentDbClientDestination -Recurse -Force + Get-ChildItem -Path $scheduledTasksRoot -Recurse -Filter *.xml | Where-Object { $_.FullName -notmatch '\\Tests\\' } | ForEach-Object { diff --git a/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 b/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 index 260f664..5d7323a 100644 --- a/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 +++ b/ScheduledTasks/DailySupportCheck/Invoke-DailySupportCheck.ps1 @@ -707,7 +707,7 @@ function Import-KurrentDbClientAssembly { [CmdletBinding()] param([Parameter(Mandatory)] [string] $AssemblyPath) - if ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'KurrentDB.Client' }) { + if ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'SupportTools.KurrentDbClient' }) { return } @@ -715,27 +715,6 @@ function Import-KurrentDbClientAssembly { throw "KurrentDB client assembly was not found: $AssemblyPath" } - $frameworkRoot = Join-Path ([Environment]::GetFolderPath('ProgramFiles')) 'dotnet\shared\Microsoft.AspNetCore.App' - $frameworkDirectory = if (Test-Path -LiteralPath $frameworkRoot) { - @( - Get-ChildItem -LiteralPath $frameworkRoot -Directory | Where-Object Name -like '9.*' | Sort-Object Name -Descending - Get-ChildItem -LiteralPath $frameworkRoot -Directory | Sort-Object Name -Descending - ) | Select-Object -First 1 - } - - foreach ($dependencyName in @( - 'Microsoft.Extensions.DependencyInjection.Abstractions.dll', - 'Microsoft.Extensions.Logging.Abstractions.dll', - 'Microsoft.Extensions.Options.dll' - )) { - if ($frameworkDirectory) { - $dependencyPath = Join-Path $frameworkDirectory.FullName $dependencyName - if (Test-Path -LiteralPath $dependencyPath -PathType Leaf) { - Add-Type -Path $dependencyPath - } - } - } - Add-Type -Path $AssemblyPath } @@ -767,11 +746,11 @@ function Test-KurrentDbWriteActivity { } $timeoutSeconds = if ($settings.PSObject.Properties['TimeoutSeconds']) { [int] $settings.TimeoutSeconds } else { 10 } - $assemblyPath = if ($settings.PSObject.Properties['ClientAssemblyPath'] -and -not [string]::IsNullOrWhiteSpace([string] $settings.ClientAssemblyPath)) { - [string] $settings.ClientAssemblyPath + $assemblyPath = if ($settings.PSObject.Properties['KurrentDbClientAssemblyPath'] -and -not [string]::IsNullOrWhiteSpace([string] $settings.KurrentDbClientAssemblyPath)) { + [string] $settings.KurrentDbClientAssemblyPath } else { - Join-Path $PSScriptRoot '..\..\StreamManagementTool\bin\Debug\net10.0\KurrentDB.Client.dll' + Join-Path $PSScriptRoot 'KurrentDbClient\SupportTools.KurrentDbClient.dll' } if (-not [IO.Path]::IsPathRooted($assemblyPath)) { $assemblyPath = [IO.Path]::GetFullPath((Join-Path (Split-Path -Parent $Context.ConfigPath) $assemblyPath)) @@ -790,54 +769,26 @@ function Test-KurrentDbWriteActivity { "esdb://$($baseUri.Host):${port}?tls=false&tlsVerifyCert=false" } - $client = [KurrentDB.Client.KurrentDBClient]::new([KurrentDB.Client.KurrentDBClientSettings]::Create($connectionString)) - $cancellationSource = [Threading.CancellationTokenSource]::new() - $cancellationSource.CancelAfter([timespan]::FromSeconds($timeoutSeconds)) - $userCredentials = $null + $password = $null if ($settings.PSObject.Properties['Username'] -and -not [string]::IsNullOrWhiteSpace([string] $settings.Username)) { $passwordEnvironmentVariable = [string] $settings.PasswordEnvironmentVariable $password = [Environment]::GetEnvironmentVariable($passwordEnvironmentVariable) if ([string]::IsNullOrWhiteSpace($password)) { throw "KurrentDB password environment variable '$passwordEnvironmentVariable' is not set." } - $userCredentials = [KurrentDB.Client.UserCredentials]::new([string] $settings.Username, $password) } - try { - $filter = [KurrentDB.Client.StreamFilter]::Prefix($indexName) - $read = $client.ReadAllAsync( - [KurrentDB.Client.Direction]::Backwards, - [KurrentDB.Client.Position]::End, - $filter, - $requestCount, - $false, - [timespan]::FromSeconds($timeoutSeconds), - $userCredentials, - $cancellationSource.Token - ) - $enumerator = $read.GetAsyncEnumerator() - $events = [System.Collections.Generic.List[object]]::new() - try { - while ($enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult()) { - $currentEvent = $enumerator.Current.Event - $events.Add([pscustomobject]@{ - EventId = [string] $currentEvent.EventId - EventType = [string] $currentEvent.EventType - Timestamp = $currentEvent.Created.ToUniversalTime().ToString('o') - Title = [string] $currentEvent.EventStreamId - }) - } - } - finally { - $enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult() | Out-Null - } - - [pscustomobject]@{ events = @($events) } - } - finally { - $cancellationSource.Dispose() - $client.Dispose() - } + $reader = [SupportTools.KurrentDbClient.KurrentDbEventReader]::new() + $events = $reader.ReadRecentEventsAsync( + $connectionString, + $indexName, + $requestCount, + [timespan]::FromSeconds($timeoutSeconds), + [string] $settings.Username, + $password, + [Threading.CancellationToken]::None).GetAwaiter().GetResult() + + [pscustomobject]@{ events = @($events) } } if ($Context.PSObject.Properties['KurrentDbWriteActivityProvider']) { $provider = $Context.KurrentDbWriteActivityProvider diff --git a/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj new file mode 100644 index 0000000..fa02fff --- /dev/null +++ b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + false + SupportTools.KurrentDbClient + + + + + + diff --git a/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbEventReader.cs b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbEventReader.cs new file mode 100644 index 0000000..3546734 --- /dev/null +++ b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbEventReader.cs @@ -0,0 +1,58 @@ +using KurrentDB.Client; + +namespace SupportTools.KurrentDbClient; + +public sealed record KurrentDbEvent( + string EventId, + string EventType, + string Timestamp, + string Title); + +public sealed class KurrentDbEventReader +{ + public async Task> ReadRecentEventsAsync( + string connectionString, + string streamPrefix, + int eventCount, + TimeSpan timeout, + string? username = null, + string? password = null, + CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); + ArgumentException.ThrowIfNullOrWhiteSpace(streamPrefix); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(eventCount); + + var settings = KurrentDBClientSettings.Create(connectionString); + using var client = new KurrentDBClient(settings); + using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutSource.CancelAfter(timeout); + + UserCredentials? credentials = null; + if (!string.IsNullOrWhiteSpace(username)) + credentials = new UserCredentials(username, password ?? string.Empty); + + var events = new List(eventCount); + var filter = StreamFilter.Prefix(streamPrefix); + + await foreach (var resolvedEvent in client.ReadAllAsync( + Direction.Backwards, + Position.End, + filter, + eventCount, + resolveLinkTos: false, + deadline: timeout, + userCredentials: credentials, + cancellationToken: timeoutSource.Token)) + { + var currentEvent = resolvedEvent.Event; + events.Add(new KurrentDbEvent( + currentEvent.EventId.ToString(), + currentEvent.EventType, + currentEvent.Created.ToUniversalTime().ToString("o"), + currentEvent.EventStreamId)); + } + + return events; + } +} diff --git a/ScheduledTasks/DailySupportCheck/README.md b/ScheduledTasks/DailySupportCheck/README.md index 2f58bb3..eebb916 100644 --- a/ScheduledTasks/DailySupportCheck/README.md +++ b/ScheduledTasks/DailySupportCheck/README.md @@ -78,7 +78,7 @@ The default configuration file is `daily-support-check.json` beside the script: "KurrentDbWriteActivity": { "Enabled": true, "BaseUrl": "http://localhost:2113", - "ClientAssemblyPath": "..\\..\\StreamManagementTool\\bin\\Debug\\net10.0\\KurrentDB.Client.dll", + "KurrentDbClientAssemblyPath": ".\\KurrentDbClient\\SupportTools.KurrentDbClient.dll", "Streams": [ { "Name": "$idx-ce-TransactionAggregate", @@ -102,7 +102,7 @@ The `KurrentDB Projections` check calls the configured `/projections/any` endpoi The `Scheduled Tasks` check uses `Get-ScheduledTask` and `Get-ScheduledTaskInfo` to inspect only the configured tasks. It fails for missing, disabled, failed, overdue, or stale tasks, and reports the task state, last result, last run, next run, and failure reason. `ExpectedRunIntervalMinutes` determines how old the last run may be; the older `MaxLastRunAgeHours` property remains supported for compatibility. The current example checks `Daily Settlement` and `Replay Parked Queue` every 24 hours, and `Scavenge` every 7 days. -The `KurrentDB Write Activity` check reads the latest events backwards from `$all` using a secondary-index prefix filter for each configured index. It reports the event IDs, event types, and timestamps for each index. Each index passes when recent events are found, warns when fewer than its `EventCount` are available, and fails when no events are returned, the query is unavailable, or its latest event is older than its `MaxLatestEventAgeMinutes`. The overall check fails if any index fails and warns if one or more indexes warn. `ClientAssemblyPath` points to `KurrentDB.Client.dll`; it is resolved relative to the configuration file when relative. The check is disabled by default until the correct secondary indexes have been configured. For compatibility, the older single-stream properties (`StreamName`, `EventCount`, and `MaxLatestEventAgeMinutes`) are also accepted. +The `KurrentDB Write Activity` check reads the latest events backwards from `$all` using a secondary-index prefix filter for each configured index. It reports the event IDs, event types, and timestamps for each index. Each index passes when recent events are found, warns when fewer than its `EventCount` are available, and fails when no events are returned, the query is unavailable, or its latest event is older than its `MaxLatestEventAgeMinutes`. The overall check fails if any index fails and warns if one or more indexes warn. `KurrentDbClientAssemblyPath` points to the packaged `SupportTools.KurrentDbClient.dll` helper and is resolved relative to the configuration file when relative. The helper owns the KurrentDB client dependency so the check does not depend on `StreamManagementTool`. The check is disabled by default until the correct secondary indexes have been configured. For compatibility, the older single-stream properties (`StreamName`, `EventCount`, and `MaxLatestEventAgeMinutes`) are also accepted. The default template checks are: diff --git a/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 b/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 index 62c7ecc..d33f8f9 100644 --- a/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 +++ b/ScheduledTasks/DailySupportCheck/Tests/Invoke-DailySupportCheck.Tests.ps1 @@ -1,4 +1,11 @@ Describe 'Daily support check' { + It 'uses the packaged KurrentDB client rather than a source-tree build output' { + $configurationPath = Join-Path $PSScriptRoot '..\daily-support-check.json' + $configuration = Get-Content -LiteralPath $configurationPath -Raw | ConvertFrom-Json + + $configuration.KurrentDbWriteActivity.KurrentDbClientAssemblyPath | Should -Be '.\KurrentDbClient\SupportTools.KurrentDbClient.dll' + } + BeforeAll { . (Join-Path $PSScriptRoot '..\Invoke-DailySupportCheck.ps1') } diff --git a/ScheduledTasks/DailySupportCheck/daily-support-check.json b/ScheduledTasks/DailySupportCheck/daily-support-check.json index 95b597a..347ba7a 100644 --- a/ScheduledTasks/DailySupportCheck/daily-support-check.json +++ b/ScheduledTasks/DailySupportCheck/daily-support-check.json @@ -50,7 +50,7 @@ "KurrentDbWriteActivity": { "Enabled": true, "BaseUrl": "http://localhost:2113", - "ClientAssemblyPath": "..\\..\\StreamManagementTool\\bin\\Debug\\net10.0\\KurrentDB.Client.dll", + "KurrentDbClientAssemblyPath": ".\\KurrentDbClient\\SupportTools.KurrentDbClient.dll", "Streams": [ { "Name": "$idx-ce-TransactionAggregate", From de4dfedc05b1ca5f35ffc4fbe699b46354411a28 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 1 Oct 2026 08:40:41 +0100 Subject: [PATCH 2/2] fix: target support helper to net8 --- .../DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj index fa02fff..2e723cd 100644 --- a/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj +++ b/ScheduledTasks/DailySupportCheck/KurrentDbClient/KurrentDbClient.csproj @@ -1,6 +1,6 @@ - net10.0 + net8.0 enable enable false