AutoMate Technisch Help

Status Dashboard — Technische Architectuur

Het Status Dashboard toont real-time gezondheid per module (IAM, UserTypes, Workflows) in het Management Portal. De implementatie is gebaseerd op een SignalR streaming architectuur die Seq parallel bevraagt en resultaten naar de portal pusht zodra ze beschikbaar zijn. Hierdoor kan het eerste kaartje al renderen terwijl de overige modules nog worden opgevraagd.

Architectuuroverzicht

SignalR stream

ValidateTenantAccessAsync

toegang geweigerd

toegang verleend

parallel - IAM

parallel - Workflows

parallel - UserTypes

miss

miss

miss

hit

hit

hit

yield

yield

yield

parallel queries

parallel queries

parallel queries

Blazor StatusPage\nStatusHubConnectionFactory

StatusHub\n/hubs/status

Tenant\nautorisatie

HubException

Channel<ModuleStatusSummary>\nunbounded

Cache hit?

Cache hit?

Cache hit?

SeqStatusService\nGetModuleStatusAsync

SeqStatusService\nGetModuleStatusAsync

SeqStatusService\nGetModuleStatusAsync

channel.Writer.WriteAsync

channel.Writer.WriteAsync

channel.Writer.WriteAsync

Seq\n/api/events

Betrokken bestanden

Component

Locatie

ISeqStatusService/SeqStatusService

API/AutoMate.API.Plugins.AuditLog/Status/

StatusSignals/DashboardSignalDefinition

API/AutoMate.API.Plugins.AuditLog/Status/StatusSignals.cs

StatusModels (domeinmodellen)

API/AutoMate.API.Plugins.AuditLog/Status/StatusModels.cs

StatusHub (SignalR hub)

API/Src/AutoMate.API/Features/Common/Status/Hubs/StatusHub.cs

StatusHubConnectionFactory

Portals/ManagementPortal/Src/ManagementPortal.Website/Services/

IStatusService/StatusService (REST fallback)

Portals/ManagementPortal/Src/ManagementPortal.Website/Services/

Blazor componenten

Portals/ManagementPortal/Src/ManagementPortal.Website/Components/Pages/Status/

Domeinmodellen

public enum ModuleHealthStatus { Ok, Warning, Error, Unknown } // Enkel Seq event (fout of audit) public sealed record StatusEvent( DateTimeOffset Timestamp, string Level, string Message, string Module, string Feature); // Samenvattting van één named signal binnen een module public sealed record StatusSignalSummary( string SignalName, string DisplayName, ModuleHealthStatus Status, int ErrorCount, DateTimeOffset? LastActivityAt, string? LastActionMessage, IReadOnlyList<StatusEvent> RecentEvents); // Gezondheidsoverzicht voor één module (IAM / Workflows / UserTypes) public sealed record ModuleStatusSummary( string ModuleName, ModuleHealthStatus Status, int ErrorCount, int WarningCount, DateTimeOffset? LastActivityAt, string? LastErrorMessage, IReadOnlyList<StatusEvent> RecentErrors, string? LastActionMessage = null, IReadOnlyList<StatusEvent>? RecentAuditEvents = null, bool IsDetailLoaded = true, IReadOnlyList<StatusSignalSummary>? Signals = null); // Aggregaat over alle modules public sealed record SystemStatusSummary( ModuleHealthStatus OverallStatus, int TotalErrorCount, int TotalWarningCount, IReadOnlyList<ModuleStatusSummary> Modules, DateTimeOffset QueriedAt);

IsDetailLoaded = false geeft aan dat een ModuleStatusSummary afkomstig is van GetModuleStatusQuickAsync — de detailvelden (RecentErrors, audit events, signals) zijn dan leeg.

Per-module query strategie

SeqStatusService.GetModuleStatusAsync voert vier categorie queries parallel uit voor elke module. Alle queries raken GET /api/events op Seq met fromDateUtc/toDateUtc en shortCircuitAfter=N.

Module-naar-log mapping

Dashboard module

Seq Module waarden

IAM

Module = 'IAM' of Module = 'Sync'

UserTypes

Module = 'OrgModel' of Module = 'UserTypes'

Workflows

Module = 'Workflows'

Query 1 — errors

Serilog error events voor foutdetectie. Retourneert maximaal 20 events.

({moduleFilter}) and TenantId = '{tenantId}' and @Level = 'Error'

Zodra er errors zijn (errors.Count > 0) wordt de module status Error; anders Ok.

Query 2 — activity

Meest recente audit event voor de LastActivityAt timestamp. Retourneert count=1.

EventKind = 'Audit' and ({moduleFilter}) and TenantId = '{tenantId}'

Query 3 — actions

Module-specifieke query voor het laatste zichtbare actiebericht in de kaartkopregel:

Module

Filter

Count

IAM

EventKind = 'Audit' and Module = 'IAM' and TenantId = '{id}'

10

UserTypes

EventKind = 'Audit' and Module = 'OrgModel' and TenantId = '{id}'

1

Workflows

({moduleFilter}) and TenantId = '{id}' and @Level = 'Information'

1

Query 4 — signals

Per module worden de signals parallel bevraagd (zie het Signal systeem hieronder). Elke signal heeft zijn eigen Seq filter; de caller voegt and TenantId = '{tenantId}' toe.

Quick variant

GetModuleStatusQuickAsync voert slechts twee queries uit (count=1, shortCircuitAfter=1) — genoeg om Ok versus Error vast te stellen en LastActivityAt te vullen. Dit is bedoeld voor snelle initiële rendering. IsDetailLoaded is altijd false.

Signal systeem

Wat is een signal?

Een DashboardSignalDefinition is een named, zelfstandig Seq filter met een weergavenaam en een maximaal aantal te retourneren events:

internal sealed record DashboardSignalDefinition( string Name, // identifier, bijv. "IamSync" string DisplayName, // weergavenaam, bijv. "IAM Sync" string SeqFilter, // Seq filter expressie (TenantId wordt door de caller toegevoegd) int Count = 5); // max events

Signals beantwoorden specifieke operationele vragen onafhankelijk van de module-brede foutdetectie: "Heeft de sync gedraaid?", "Zijn er accounts via het formulier gewijzigd?", "Heeft AutoLeave accounts uitgeschakeld?"

Gedefinieerde signals per module

IAM signals:

Signal (Name)

DisplayName

Seq filter

Vraag

IamSignals.Sync

IAM Sync

EventKind = 'Audit' and Process = 'IamSync'

Heeft de sync gedraaid?

IamSignals.EmployeeManagement

Employee Management

EventKind = 'Audit' and Module = 'IAM' and Process is null

Zijn medewerkers via het formulier gewijzigd?

IamSignals.AutoLeave

Auto Leave

EventKind = 'Audit' and Process = 'AutoLeave'

Heeft AutoLeave accounts uitgeschakeld?

UserTypes signals:

Signal (Name)

DisplayName

Seq filter

UserTypesSignals.OrgModel

Org Model

EventKind = 'Audit' and Module = 'OrgModel'

UserTypesSignals.Personas

Personas

EventKind = 'Audit' and Module = 'UserTypes'

Workflows signals:

Signal (Name)

DisplayName

Seq filter

WorkflowsSignals.Definitions

Definitions

Module = 'Workflows' and Feature = 'Definitions'

WorkflowsSignals.Executions

Executions

Module = 'Workflows' and Feature = 'Executions'

Signal gezondheid

BuildSignalSummary bepaalt de status van een signal op basis van de ontvangen events:

Conditie

Status

Errors aanwezig

Error

Events aanwezig, geen errors

Ok

Geen events

Unknown

EventKind = 'Audit' strategie — immuun voor library-ruis

Het probleem

Serilog library loggers (MSAL token acquisitie, HttpClient, EF Core etc.) worden uitgevoerd in dezelfde job context als de business code. Ze erven daardoor properties zoals Module en TenantId uit de ambient log context. Een query als Module = 'IAM' and @Level = 'Information' zou daardoor MSAL token logs kunnen teruggeven die niets te maken hebben met IAM business activiteit.

De oplossing

SeqAuditLogSink stampt op elk audit event de property EventKind = 'Audit'. Library loggers schrijven nooit deze property — zij schrijven standaard Serilog events zonder EventKind.

Alle signals filteren altijd op EventKind = 'Audit'. Hierdoor:

  • Retourneren signals uitsluitend business audit records

  • Library-ruis (token logs, HTTP logs) verschijnt nooit in signal resultaten

  • De Process property onderscheidt de trigger bron:

    • Process = 'IamSync' — geschreven tijdens een IAM sync Hangfire job

    • Process = 'AutoLeave' — geschreven tijdens de AutoLeave check job

    • Process is null — getriggerd door een mens via het AutoMate formulier of de API

Praktisch voorbeeld

MSAL logt tijdens een IAM sync:

@t: 2025-01-15T08:32:01Z @l: Information @mt: "Acquiring token for scope {Scope}" Module: IAM ← geërfd uit ambient log context TenantId: abc-123 ← geërfd uit ambient log context ← GEEN EventKind property

Een IAM Sync audit event:

@t: 2025-01-15T08:32:05Z @l: Information EventKind: Audit ← gestempeld door SeqAuditLogSink Process: IamSync Module: IAM TenantId: abc-123 EntityDisplayName: jan.jansen@example.com Action: Created

De signal filter EventKind = 'Audit' and Process = 'IamSync' geeft alleen het tweede event terug.

SignalR streaming — progressief laden

Hub: StatusHub

Locatie: API/Src/AutoMate.API/Features/Common/Status/Hubs/StatusHub.cs Hub URL: /hubs/status Authenticatie: [Authorize] attribuut

De server-streaming methode StreamModuleStatuses werkt als volgt:

public async IAsyncEnumerable<ModuleStatusSummary> StreamModuleStatuses( Guid tenantId, TimeSpan? window, [EnumeratorCancellation] CancellationToken ct) { if (!await ValidateTenantAccessAsync(tenantId)) throw new HubException("Not authorized to access this tenant"); var effectiveWindow = window ?? TimeSpan.FromHours(24); // Unbounded channel — alle 3 producers schrijven zonder elkaar te blokkeren var channel = Channel.CreateUnbounded<ModuleStatusSummary>(); var tasks = new[] { "IAM", "Workflows", "UserTypes" }.Select(async module => { var cacheKey = $"status:module:{module}:{tenantId}:{(int)effectiveWindow.TotalMinutes}"; if (!cache.TryGetValue(cacheKey, out ModuleStatusSummary? result)) { result = await statusService.GetModuleStatusAsync(module, tenantId, effectiveWindow, ct); if (result.Status != ModuleHealthStatus.Unknown) cache.Set(cacheKey, result, TimeSpan.FromSeconds(60)); } await channel.Writer.WriteAsync(result!, ct); }).ToArray(); _ = Task.WhenAll(tasks).ContinueWith( _ => channel.Writer.Complete(), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); await foreach (var summary in channel.Reader.ReadAllAsync(ct)) yield return summary; }

Volgorde van renderen: De drie modules worden parallel bevraagd. De snelste module verschijnt als eerste in de portal — er is geen vaste volgorde. De frontend rendert elk kaartje zodra het ModuleStatusSummary object binnenkomt via de stream.

Channel keuze: Een UnboundedChannel zorgt ervoor dat geen van de drie producers op de andere hoeft te wachten om te schrijven. De consumer leest in volgorde van aankomst.

Cancellation: De Seq HTTP aanroepen binnen SeqStatusService gebruiken CancellationToken.None — niet de caller token. Dit voorkomt dat portaalnavigatie een lopende Seq query afbreekt voordat het resultaat gecached kan worden.

Portal verbinding: StatusHubConnectionFactory

Locatie: Portals/ManagementPortal/Src/ManagementPortal.Website/Services/StatusHubConnectionFactory.cs

var connection = new HubConnectionBuilder() .WithUrl(hubUrl, options => { options.AccessTokenProvider = async () => { var result = await tokenManager.GetAccessTokenAsync(user); return result.WasSuccessful(out var t) ? t.AccessToken : null; }; }) .WithAutomaticReconnect() .Build();

De factory haalt de API base URL op uit services:api:https:0 of services:api:https in de configuratie. De bearer token wordt per request opgehaald via IUserTokenManager (Duende Access Token Management). WithAutomaticReconnect() herstelt de verbinding automatisch bij tijdelijke netwerkstoringen.

Caching

Cache sleutel

status:module:{module}:{tenantId}:{minutes}

Voorbeeld: status:module:IAM:550e8400-e29b-41d4-a716-446655440000:1440

Cache gedrag

  • TTL: 60 seconden per module/tenant combinatie

  • Alleen gecached als Status != Unknown — bij Seq-uitval wordt geen stale Unknown gecached

  • De REST endpoints (GET /api/status/system, GET /api/status/modules/{module}) gebruiken dezelfde cache sleutel als de SignalR hub, zodat herhaalde loads near-instant zijn

Tenant autorisatie

ValidateTenantAccessAsync in StatusHub kent twee toegangspaden:

nee

ja

ja - directe match

nee - cross-tenant

nee

ja

nee

ja

nee

ja - directe klant

StreamModuleStatuses aangeroepen

User heeft\ntenant claim?

false

userTenantId ==\nrequestedTenantId?

true

Gebruikers tenant\ntype = MSP?

Gebruiker heeft\nAutomate365.Customers.ReadWrite rol?

requestedTenant.Partner.Id\n== userTenantId?

Directe match: De tenantid claim in het JWT token komt overeen met de opgevraagde tenant ID.

MSP cross-tenant: Vereist:

  1. Gebruikerstenant heeft TenantType = "MSP"

  2. Gebruiker heeft de rol Automate365.Customers.ReadWrite

  3. De opgevraagde tenant is een directe klant van de MSP (requestedTenant.Partner.Id == userTenantId) — dit voorkomt dat een MSP toegang krijgt tot tenants van andere MSPs via SystemTenantHasAccess constructies

REST fallback

Naast SignalR biedt IStatusService een REST interface via de auto-gegenereerde Kiota API client:

Methode

Endpoint

Beschrijving

GET

/api/status/system

Alle modules in één antwoord

GET

/api/status/modules/{module}

Één module opvragen

De REST interface levert dezelfde data als de SignalR stream maar in één gebundeld antwoord. Geschikt als fallback of voor polling-gebaseerde integraties.

Nieuwe signal toevoegen — stap voor stap

1. Voeg een constante toe in LoggingConstants

Signalnamen zijn constanten genest onder het betreffende module-blok in LoggingConstants.DashboardModules:

// Voorbeeld: nieuw AutoSign signal aan IAM toevoegen public static class IamSignals { public const string Sync = "IamSync"; public const string EmployeeManagement = "EmployeeManagement"; public const string AutoLeave = "AutoLeave"; public const string AutoSign = "AutoSign"; // nieuw }

2. Registreer de DashboardSignalDefinition in StatusSignals

Voeg de definitie toe aan de juiste signals lijst in API/AutoMate.API.Plugins.AuditLog/Status/StatusSignals.cs:

private static readonly IReadOnlyList<DashboardSignalDefinition> IamSignals = [ // ... bestaande signals ... new( Name: LoggingConstants.DashboardModules.IamSignals.AutoSign, DisplayName: "AutoSign", SeqFilter: $"EventKind = 'Audit' and Process = 'AutoSign'", Count: 5), ];

Vuistregels voor de Seq filter:

  • Begin altijd met EventKind = 'Audit' voor audit-gebaseerde signals — nooit weglaten

  • Gebruik Process = 'NaamVanJob' als het signal een Hangfire-job vertegenwoordigt

  • Gebruik Process is null als het signal handmatige portaalacties vertegenwoordigt

  • Gebruik Module = '...' of Feature = '...' om verder te scopen

  • Voeg TenantId niet toe — de caller doet dat automatisch

3. Verifieer in Seq

Test de filter voordat je deployt:

EventKind = 'Audit' and Process = 'AutoSign' and TenantId = '<jouw-tenant-id>'

Vervang <jouw-tenant-id> door een bekende tenant GUID. Zie de sectie Seq filter voorbeelden hieronder voor meer debugging queries.

4. Geen verdere wijzigingen nodig

SeqStatusService.GetModuleStatusAsync laadt signals dynamisch via StatusSignals.ForModule(dashboardModule). De portal rendert signal sub-rijen automatisch op basis van de Signals property van ModuleStatusSummary.

Seq query parameters — referentie

SeqStatusService bouwt elke query als volgt op:

GET {seqServerUrl}/api/events ?filter={url-encoded filter} &fromDateUtc={ISO8601 UTC} &toDateUtc={ISO8601 UTC} &count={n} &shortCircuitAfter={n} &render=true

Parameter

Beschrijving

fromDateUtc/toDateUtc

Tijdvenster (niet fromUtc/toUtc)

count

Maximum aantal te retourneren events

shortCircuitAfter

Stop scannen zodra N matches gevonden — zeer effectief voor count=1 checks

render=true

Seq rendert berichten server-side (template tokens worden ingevuld)

X-Seq-ApiKey

API key via request header

Seq filter voorbeelden voor debugging

Vervang <tenant-id> door de betreffende tenant GUID.

Controleer of IAM sync heeft gedraaid in de afgelopen 24 uur:

EventKind = 'Audit' and Process = 'IamSync' and TenantId = '<tenant-id>'

Bekijk handmatige IAM-wijzigingen van vandaag:

EventKind = 'Audit' and Module = 'IAM' and Process is null and TenantId = '<tenant-id>'

AutoLeave activiteit:

EventKind = 'Audit' and Process = 'AutoLeave' and TenantId = '<tenant-id>'

Alle errors voor een module (zoals het dashboard ze ziet):

(Module = 'IAM' or Module = 'Sync') and TenantId = '<tenant-id>' and @Level = 'Error'

Workflow definitie wijzigingen:

Module = 'Workflows' and Feature = 'Definitions' and TenantId = '<tenant-id>'

OrgModel audit events:

EventKind = 'Audit' and Module = 'OrgModel' and TenantId = '<tenant-id>'

Controleer of library-ruis door signals heen lekt (verwacht: nul resultaten):

EventKind = 'Audit' and Process = 'IamSync' and @mt has 'token'
Last modified: 05 April 2026