diff --git a/README.md b/README.md index 5280304..93a0116 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SQLInsight Pro er et overvågningsværktøj til Microsoft SQL Server, skrevet i C# på .NET. Det kører hos kunden og forlader ikke netværket, medmindre kunden selv vælger det. Værktøjet nøjes ikke med at vise tal. Det forklarer, hvad der er galt, hvorfor det er et problem, og hvordan det løses. Rådene har samme niveau som dem, man får fra en erfaren DBA. -> **Status:** Fase 1–4 er implementeret: agent, 82 indbyggede regler plus community-regler, notifikationer, lokalt repository og web-UI, licenser, signerede opdateringer, SQLInsight Hub med portal, erfaring på tværs af kunder, PDF-rapporter og integration med Prometheus, Grafana og SCOM, plus MSI-installer, community-websted og Discourse-forum. Se [installation](docs/installation.md), [opdateringer](docs/opdatering.md), [integration](docs/integration.md), [rapporter](docs/rapporter.md), [egne regler](docs/regler.md), [databehandling](docs/databehandling.md), [community](docs/community.md), [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md). +> **Status:** Fase 1–4 er implementeret: agent, 92 indbyggede regler plus community-regler, notifikationer, lokalt repository og web-UI, licenser, signerede opdateringer, SQLInsight Hub med portal, erfaring på tværs af kunder, PDF-rapporter og integration med Prometheus, Grafana og SCOM, plus MSI-installer, community-websted og Discourse-forum. Se [installation](docs/installation.md), [opdateringer](docs/opdatering.md), [integration](docs/integration.md), [rapporter](docs/rapporter.md), [egne regler](docs/regler.md), [databehandling](docs/databehandling.md), [community](docs/community.md), [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md). --- diff --git a/SQL Scripts/SQLI-AX-001.sql b/SQL Scripts/SQLI-AX-001.sql new file mode 100644 index 0000000..3bb0ee4 --- /dev/null +++ b/SQL Scripts/SQLI-AX-001.sql @@ -0,0 +1,152 @@ +/* + Regel: SQLI-AX-001 – Dynamics AX-batchjob er endt med fejl + Alvorlighed: Høj (High) + + Hvad og hvorfor + Et batchjob i Dynamics AX er stoppet med fejl. Det, jobbet skulle gøre (bogføring, lagerafslutning, integration …), + er ikke gjort eller kun delvist, og gentagne job fortsætter ikke nødvendigvis af sig selv. + + Tærskel (agentens konfiguration SQLInsight:DynamicsAx) + BatchFailedLookbackHours = 24 (standard): fejl inden for så mange timer rapporteres. + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxBatchFailedRule) + Et fund pr. job, når jobbet nu har status Fejl og sluttede (eller var planlagt) inden for tærsklen, eller når jobbet + venter på næste gentagelse, men dets seneste kørsel i SQLInsights egen historik (insight.AxBatchRuns) endte med fejl + inden for tærsklen. STATUS oversættes af versionens adapter: AX 2009+ 0 Tilbageholdt, 1 Venter, 2 Udfører, 3 Fejl, + 4 Afsluttet, 5 Klar, 6 Ikke kørt, 7 Annullerer, 8 Annulleret; Axapta 3.0/4.0 (antaget) 0 Venter, 1 Udfører, 2 Fejl, + 3 Afsluttet, 4 Tilbageholdt. Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs – AxBatchCollector.CollectAsync (hvert 5. minut). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – BatchJobs og BatchHistory. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO []; + AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO []; + AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO []; + GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen (også under Dynamics AX → + AX-databaser og GET /api/v1/dynamics-ax/grant-script). Genkendelsen kræver kun grant-least-privilege.sql. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (docs/dynamics-ax/ + schema-ax30.tsv); STATUS-værdierne er dog ikke verificeret mod kundedata. AX 4.0, AX 2009, AX 2012 og F&O: + baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-002.sql b/SQL Scripts/SQLI-AX-002.sql new file mode 100644 index 0000000..0d18f60 --- /dev/null +++ b/SQL Scripts/SQLI-AX-002.sql @@ -0,0 +1,152 @@ +/* + Regel: SQLI-AX-002 – Dynamics AX-batchjob kører længere end normalt + Alvorlighed: Medium; Høj (High), når kørslen er dobbelt så lang som grænsen + + Hvad og hvorfor + Et kørende batchjob har kørt meget længere end jobbets egne seneste kørsler. Det kan skyldes blokering, en ændret + datamængde, en dårlig plan, eller at jobbet hænger – og imens kan det holde låse, som brugerne venter på. + + Tærskler (agentens konfiguration SQLInsight:DynamicsAx) + BatchLongRunningFactor = 2.0 kørt mere end så mange gange jobbets 90 %-fraktil + BatchLongRunningMinimumMinutes = 10 men aldrig under så mange minutter + BatchHistoryMinimumRuns = 5 så mange tidligere kørsler skal der være + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxBatchLongRunningRule) + For job med status Udfører: kørt = nu − faktisk start (AX 2009+: BATCHJOB.STARTDATETIME; Axapta 3.0/4.0 gemmer ikke + den faktiske start, så det er første gang SQLInsight så jobbet køre). Grænse = max(faktor × 90 %-fraktil af jobbets + seneste op til 20 vellykkede kørsler, minimum). Historikken er SQLInsights egen (insight.AxBatchRuns): fra + BATCHJOBHISTORY i AX 2009+, ellers de kørsler SQLInsight selv ser blive afsluttet. Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs – AxBatchCollector.CollectAsync (hvert 5. minut). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – BatchJobs og BatchHistory. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO []; + AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO []; + AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO []; + GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema; STATUS-værdierne er dog + ikke verificeret mod kundedata. AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede + skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-003.sql b/SQL Scripts/SQLI-AX-003.sql new file mode 100644 index 0000000..ee0c679 --- /dev/null +++ b/SQL Scripts/SQLI-AX-003.sql @@ -0,0 +1,150 @@ +/* + Regel: SQLI-AX-003 – Dynamics AX-batchjob er ikke startet til tiden + Alvorlighed: Medium; Høj (High), når jobbet i praksis aldrig er startet + + Hvad og hvorfor + Et ventende batchjob, hvis planlagte start er passeret: batchserveren (AOS med batch, eller batch-klienten i + Axapta 3.0) tager det ikke. Mange forsinkede job på én gang betyder typisk, at batchserveren slet ikke kører. + + Tærskler (agentens konfiguration SQLInsight:DynamicsAx) + BatchLateMinutes = 30 forsinket mere end så mange minutter: Medium + BatchNeverStartedHours = 24 forsinket mere end så mange timer: Høj, "aldrig startet" + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxBatchLateRule) + For job med status Venter: forsinkelse = nu − planlagt start. Planlagt start er i AX 2009+ BATCHJOB.ORIGSTARTDATETIME + (UTC). I Axapta 3.0/4.0 er den BATCH.STARTDATE (lokal dato) + STARTTIME (sekunder efter midnat, lokal tid); den regnes + om til UTC med SQL Servers forskel mellem GETDATE() og GETUTCDATE() på læsetidspunktet. AX' tomme dato (1900-01-01) + er "ingen". Job med status Tilbageholdt er ikke forsinkede. Højst 25 fund pr. database, de mest forsinkede først. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs – AxBatchCollector.CollectAsync (hvert 5. minut). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – BatchJobs. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO []; + AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO []; + AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema; STATUS-værdierne er dog + ikke verificeret mod kundedata. AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede + skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-004.sql b/SQL Scripts/SQLI-AX-004.sql new file mode 100644 index 0000000..831b024 --- /dev/null +++ b/SQL Scripts/SQLI-AX-004.sql @@ -0,0 +1,150 @@ +/* + Regel: SQLI-AX-004 – Dynamics AX-batchjob hænger i Udfører + Alvorlighed: Høj (High) + + Hvad og hvorfor + Et batchjob har haft status Udfører i flere timer. Typisk er AOS'en eller batch-klienten genstartet eller gået ned midt + i kørslen, så jobbet aldrig blev afsluttet: det står som kørende, intet sker, og gentagne job planlægges ikke igen. + + Tærskler (agentens konfiguration SQLInsight:DynamicsAx) + BatchStuckHours = 6 kørt mindst så mange timer + BatchLongRunningFactor = 2.0 …og mere end så mange gange jobbets 90 %-fraktil, når der er historik + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxBatchStuckRule) + For job med status Udfører: kørt = nu − faktisk start (AX 2009+: BATCHJOB.STARTDATETIME; Axapta 3.0/4.0: første gang + SQLInsight så jobbet køre, så det kan have kørt længere). Fund, når kørt ≥ BatchStuckHours, medmindre jobbet efter + sin egen historik (insight.AxBatchRuns) normalt kører så længe (kørt ≤ faktor × 90 %-fraktil). + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs – AxBatchCollector.CollectAsync (hvert 5. minut). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – BatchJobs og BatchHistory. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[BATCH] TO []; + AX 4.0: GRANT SELECT ON [dbo].[BATCH] TO []; + AX 2009/2012/F&O: GRANT SELECT ON [dbo].[BATCHJOB] TO []; + GRANT SELECT ON [dbo].[BATCHJOBHISTORY] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema; STATUS-værdierne er dog + ikke verificeret mod kundedata. AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede + skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-005.sql b/SQL Scripts/SQLI-AX-005.sql new file mode 100644 index 0000000..c1c2523 --- /dev/null +++ b/SQL Scripts/SQLI-AX-005.sql @@ -0,0 +1,96 @@ +/* + Regel: SQLI-AX-005 – Dynamics AX-nummerserie er ved at løbe tør + Alvorlighed: Medium; Høj (High) over den kritiske procent eller inden for de kritiske dage; Kritisk (Critical), når serien er opbrugt + + Hvad og hvorfor + Når en nummerserie er brugt op, kan AX ikke tildele flere numre: oprettelse eller bogføring af fx ordrer, fakturaer + eller posteringer, der bruger serien, fejler for alle brugere. + + Tærskler (agentens konfiguration SQLInsight:DynamicsAx) + NumberSequenceWarnPercent = 80 brugt mindst så mange procent: Medium + NumberSequenceCriticalPercent = 95 …: Høj + ForecastWarnDays = 90 løber tør inden for så mange dage efter prognosen: Medium + ForecastCriticalDays = 30 …: Høj + ForecastBasisDays = 30 prognosen bruger den ældste daglige måling inden for så mange dage (mindst 1 dag gammel) + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxNumberSequenceExhaustionRule; tal i + src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – Interpret og AxForecast.From) + Grænse = HIGHEST, eller formatets kapacitet (10^antal # − 1), hvis den er lavere. Brugt % = (NEXTREC − LOWEST) / + (grænse − LOWEST + 1). Tilbage = grænse − NEXTREC + 1. Prognose: forbrug pr. dag = (NEXTREC nu − NEXTREC i den ældste + daglige måling i vinduet) / dage imellem; dage tilbage = (grænse − NEXTREC) / forbrug pr. dag. Målingerne er SQLInsights + egne (insight.AxSequenceSamples, én pr. serie pr. dag, gemt i HourlyDays = 400 dage). Manuelle, cykliske og spærrede + serier springes over. Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync (hver time). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – NumberSequences. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO []; + GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO []; (valgfri) + AX 4.0/2009: samme tabeller i [dbo] + AX 2012/F&O: GRANT SELECT ON [dbo].[NUMBERSEQUENCETABLE] TO []; + GRANT SELECT ON [dbo].[NUMBERSEQUENCESCOPE] TO []; (valgfri: firma) + GRANT SELECT ON [dbo].[NUMBERSEQUENCEREFERENCE] TO []; (valgfri) + Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (schema-ax30.tsv). + AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP (de mest brugte først), READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen + tillader det), aldrig NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og + valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-006.sql b/SQL Scripts/SQLI-AX-006.sql new file mode 100644 index 0000000..b205fa2 --- /dev/null +++ b/SQL Scripts/SQLI-AX-006.sql @@ -0,0 +1,83 @@ +/* + Regel: SQLI-AX-006 – Dynamics AX-nummerseriens format er for kort til Højeste + Alvorlighed: Medium; Høj (High), når NEXTREC allerede er større end formatets kapacitet + + Hvad og hvorfor + Formatet (fx "FA-####") har færre cifre, end HIGHEST kræver. Når serien når forbi formatets kapacitet, fejler + tildelingen af numre, selv om serien ikke er brugt op. + + Tærskler: ingen (ren sammenligning). + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxNumberSequenceFormatRule; tal i + src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – Interpret) + Kapacitet = 10^(antal # i FORMAT) − 1. Fund, når HIGHEST > kapacitet (ikke for manuelle serier). Høj, når også + NEXTREC > kapacitet. Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync (hver time). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – NumberSequences. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO []; + AX 4.0/2009/2012/F&O: GRANT SELECT ON [dbo].[NUMBERSEQUENCETABLE] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema. AX 4.0, AX 2009, AX 2012 + og F&O: baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-007.sql b/SQL Scripts/SQLI-AX-007.sql new file mode 100644 index 0000000..9de61ca --- /dev/null +++ b/SQL Scripts/SQLI-AX-007.sql @@ -0,0 +1,84 @@ +/* + Regel: SQLI-AX-007 – Spærret Dynamics AX-nummerserie bruges stadig + Alvorlighed: Medium + + Hvad og hvorfor + Serien er spærret (BLOCKED = 1), men en nummerseriereference peger stadig på den. Alt i AX, der skal have et nummer + fra den reference, fejler med en fejl om, at nummerserien er spærret. + + Tærskler: ingen. + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxNumberSequenceBlockedRule) + Fund, når BLOCKED = 1 og der findes en NUMBERSEQUENCEREFERENCE til serien (Axapta 3.0/4.0/2009: samme NUMBERSEQUENCE + og DATAAREAID; AX 2012/F&O: NUMBERSEQUENCEID = serieens RECID). Kan referencetabellen ikke læses, vides det ikke, og + der gives intet fund (datanoten under Dynamics AX → Nummerserier siger det). Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync (hver time). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – NumberSequences. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO []; + GRANT SELECT ON [bmssa].[NUMBERSEQUENCEREFERENCE] TO []; + AX 4.0/2009/2012/F&O: de samme tabeller i [dbo] + Mangler NUMBERSEQUENCETABLE, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema. AX 4.0, AX 2009, AX 2012 + og F&O: baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-008.sql b/SQL Scripts/SQLI-AX-008.sql new file mode 100644 index 0000000..f2ce828 --- /dev/null +++ b/SQL Scripts/SQLI-AX-008.sql @@ -0,0 +1,86 @@ +/* + Regel: SQLI-AX-008 – Kontinuerlig Dynamics AX-nummerserie er ikke ryddet op + Alvorlighed: Lav (Low) + + Hvad og hvorfor + En kontinuerlig serie må ikke have huller. Numre reserveret af afbrudte sessioner bliver liggende som optagede, til + serien ryddes op; uden oprydning opstår huller og fejl, og serien bliver en flaskehals, fordi hver tildeling låser + rækken i NUMBERSEQUENCETABLE, til transaktionen er færdig. + + Tærskler + 7 dage (fast, AxNumberSequenceContinuousRule.MinimumDays), eller 2 × seriens eget CLEANINTERVAL (timer), hvis længere. + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxNumberSequenceContinuousRule) + For kontinuerlige (CONTINUOUS = 1), ikke spærrede, ikke manuelle serier, der er taget i brug (NEXTREC > LOWEST): fund, + når seneste oprydning er ældre end tærsklen eller aldrig er sket (1900-01-01). Seneste oprydning er + LATESTCLEANDATETIME (UTC) i AX 2009+, og LATESTCLEANDATE + LATESTCLEANTIME (lokal tid, sekunder efter midnat) i + Axapta 3.0. Mangler kolonnen, gives intet fund. Højst 25 fund pr. database. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync (hver time). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – NumberSequences. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO []; + AX 4.0/2009/2012/F&O: GRANT SELECT ON [dbo].[NUMBERSEQUENCETABLE] TO []; + Mangler den, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema. AX 4.0, AX 2009, AX 2012 + og F&O: baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-009.sql b/SQL Scripts/SQLI-AX-009.sql new file mode 100644 index 0000000..75dbdec --- /dev/null +++ b/SQL Scripts/SQLI-AX-009.sql @@ -0,0 +1,94 @@ +/* + Regel: SQLI-AX-009 – Dynamics AX RecId er ved at løbe tør + Alvorlighed: Axapta 3.0 (32-bit): Høj (High) fra advarselsgrænsen, Kritisk (Critical) fra den kritiske grænse; + 64-bit: Høj (High); altid Kritisk, når RecId er opbrugt + + Hvad og hvorfor + Når en tabels RecId-serie er brugt op, kan AX ikke oprette flere poster. Axapta 3.0 har 32-bit RecId (int, højst + 2.147.483.647 pr. tabel og firma), og det løses kun med et stort projekt – derfor advares langt tidligere for 3.0. + + Tærskler (agentens konfiguration SQLInsight:DynamicsAx) + RecId32WarnPercent = 50 Axapta 3.0: Høj fra så mange procent af int-intervallet + RecId32CriticalPercent = 75 Axapta 3.0: Kritisk + RecId32ForecastDays = 730 Axapta 3.0: Høj, hvis den løber tør inden for så mange dage; Kritisk inden for halvdelen + RecId64WarnPercent = 80 64-bit (eller sænket MAXVAL): Høj + ForecastWarnDays = 90 64-bit: Høj, hvis den løber tør inden for så mange dage + ForecastBasisDays = 30 prognosen bruger den ældste daglige måling inden for så mange dage + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxRecIdExhaustionRule.Assess; tal i + src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – Interpret(AxRecIdRow)) + Grænse = typens maksimum (int, når SYSTEMSEQUENCES.NEXTVAL er int som i Axapta 3.0; ellers bigint), eller MAXVAL, hvis + den er sat lavere. Brugt % = NEXTVAL / grænse. Prognose ud fra SQLInsights egne daglige målinger af NEXTVAL + (insight.AxSequenceSamples). Højst 25 fund pr. database, de mest brugte først. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync (hver time). + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – RecIds. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: GRANT SELECT ON [bmssa].[SYSTEMSEQUENCES] TO []; + GRANT SELECT ON [bmssa].[SQLDICTIONARY] TO []; (valgfri: tabelnavne) + AX 4.0/2009/2012/F&O: de samme tabeller i [dbo] + Mangler SYSTEMSEQUENCES, er reglen "ikke målbar" med den nøjagtige GRANT for databasen. + Versioner: SQL Server 2005 og nyere (ikke 2000). Axapta 3.0: verificeret mod en kundes skema (SYSTEMSEQUENCES med + NEXTVAL/MAXVAL pr. TABID og DATAAREAID). AX 4.0, AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede + skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP (de højeste først), READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen + tillader det), aldrig NOLOCK, i AX-databasen via [db].sys.sp_executesql. Kun i databaser genkendt som AX og + valgt til overvågning. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [bmssa].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [bmssa].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/SQLI-AX-010.sql b/SQL Scripts/SQLI-AX-010.sql new file mode 100644 index 0000000..2b64304 --- /dev/null +++ b/SQL Scripts/SQLI-AX-010.sql @@ -0,0 +1,185 @@ +/* + Regel: SQLI-AX-010 – AX-bruger blokerer andre + Alvorlighed: Medium; Høj (High) ved ventetid over 5 minutter eller mindst 10 blokerede sessioner + + Hvad og hvorfor + En session fra Dynamics AX holder låse, som andre AX-brugere eller batchjob venter på. Alle AX-brugere går gennem + AOS'ens login, så SQL Server-login'et siger ikke, hvem det er. Reglen viser AX-brugeren, når SQL Server kan se den. + + Tærskel (agentens konfiguration SQLInsight:DynamicsAx) + BlockingSeconds = 30 den længste ventetid bag sessionen skal være mindst så mange sekunder + + Vurdering (C#: src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs – AxBlockingUserRule; brugeren findes af + src/SQLInsight.Core/DynamicsAx/AxSessionMapper.cs) + For hver head blocker, hvis egen database eller en blokeret sessions database er genkendt som AX: fund, når den + længste ventetid i kæden er over tærsklen. AX 2009/2012/F&O: AX-brugeren og AX-sessionen læses fra CONTEXT_INFO, som + AOS'en sætter, når "connectioncontext" er slået til i AOS-konfigurationen (første ord med bogstaver = bruger, første + tal = AX-session; formatet er dokumenteret af Microsoft for AX 2012 – ikke verificeret mod kundedata). Axapta 3.0 og + AX 4.0 sætter aldrig CONTEXT_INFO: brugeren kan ikke ses fra SQL Server, og fundet siger det ærligt og viser vært + (AOS'en ved 3-lags, brugerens pc ved 2-lags-klient) og program. AX-brugeren er følsom (sendes ikke til hubben uden + tilladelse). + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs – AxDatabaseDetector.GetAsync (via DynamicsAxCollector, + hver time) og src/SQLInsight.Collectors/Concurrency/BlockingCollector.cs – BuildSql2005/Sql2000 (hvert minut). + Rettigheder: Ingen ud over grant-least-privilege.sql (VIEW SERVER STATE til sessionerne, VIEW ANY DEFINITION til + genkendelsen). Ingen AX-læseadgang nødvendig. + Versioner: Blokering: SQL Server 2000 og nyere; AX-genkendelsen og dermed reglen: SQL Server 2005 og nyere. + Axapta 3.0/4.0: AX-brugeren kan ikke ses. AX 2009/2012/F&O: baseret på Microsofts dokumentation – ikke + verificeret mod kundedata. + Læsning: Kun DMV'er og metadata. Genkendelsen kører i hver overvåget database via [db].sys.sp_executesql. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Alle versioner: online brugerdatabaser og deres snapshot-indstillinger (AxDatabaseDetector.DatabasesSql) +SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on +FROM sys.databases +WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 +ORDER BY name +GO + +-- Variant: Alle versioner: agentens login, til GRANT-teksten +SELECT SUSER_SNAME() +GO + +-- Variant: SQL Server 2005 og nyere: AX-genkendelse i hver overvåget database (AxDetection.MetadataSql via AxSql.InDatabase) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til databasen +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length, + ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0), + (SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID()) +FROM sys.tables t +JOIN sys.schemas s ON s.schema_id = t.schema_id +JOIN sys.columns c ON c.object_id = t.object_id +WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'') +ORDER BY s.name, t.name, c.column_id'; +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: SQL Server 2000 (kun opdagelse under Servere og databaser, AxDetection.MetadataSql2000) +SELECT u.name, UPPER(o.name), UPPER(c.name), TYPE_NAME(c.xtype), c.xprec, c.length, 0, NULL +FROM [AXDB].dbo.sysobjects o +JOIN [AXDB].dbo.sysusers u ON u.uid = o.uid +JOIN [AXDB].dbo.syscolumns c ON c.id = o.id +WHERE o.xtype = 'U' AND UPPER(o.name) IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG', N'BATCH', N'BATCHJOB', N'BATCHJOBHISTORY', N'BATCHGROUP', N'NUMBERSEQUENCETABLE', N'NUMBERSEQUENCESCOPE', N'NUMBERSEQUENCEREFERENCE', N'SYSTEMSEQUENCES', N'MODELELEMENT') +ORDER BY u.name, o.name, c.colid +GO + +-- Variant: Axapta 3.0 (skema bmssa): firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [bmssa].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: SQL Server 2012 og nyere (BlockingCollector.BuildSql2005) +SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource, + DB_NAME(COALESCE(r.database_id, s.database_id)), s.login_name, s.host_name, s.program_name, s.open_transaction_count, + LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL + THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1, + (CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END + - r.statement_start_offset) / 2 + 1) + ELSE t.text END, 4000), + s.last_request_end_time, s.context_info +FROM sys.dm_exec_sessions s +LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id +LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id +OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t +WHERE r.blocking_session_id > 0 + OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0) +GO + +-- Variant: SQL Server 2008 og 2008 R2 +SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource, + DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id), + LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL + THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1, + (CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END + - r.statement_start_offset) / 2 + 1) + ELSE t.text END, 4000), + s.last_request_end_time, s.context_info +FROM sys.dm_exec_sessions s +LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id +LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id +OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t +WHERE r.blocking_session_id > 0 + OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0) +GO + +-- Variant: SQL Server 2005 +SELECT s.session_id, NULLIF(r.blocking_session_id, 0), COALESCE(r.status, s.status), r.wait_type, r.wait_time, r.wait_resource, + DB_NAME(r.database_id), s.login_name, s.host_name, s.program_name, (SELECT COUNT(*) FROM sys.dm_tran_session_transactions st WHERE st.session_id = s.session_id), + LEFT(CASE WHEN r.sql_handle IS NOT NULL AND r.statement_start_offset IS NOT NULL + THEN SUBSTRING(t.text, r.statement_start_offset / 2 + 1, + (CASE WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END + - r.statement_start_offset) / 2 + 1) + ELSE t.text END, 4000), + s.last_request_end_time, (SELECT TOP 1 p.context_info FROM sys.sysprocesses p WHERE p.spid = s.session_id AND p.ecid = 0) +FROM sys.dm_exec_sessions s +LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id +LEFT JOIN sys.dm_exec_connections c ON c.session_id = s.session_id +OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, c.most_recent_sql_handle)) t +WHERE r.blocking_session_id > 0 + OR s.session_id IN (SELECT blocking_session_id FROM sys.dm_exec_requests WHERE blocking_session_id > 0) +GO + +-- Variant: SQL Server 2000 (BlockingCollector.Sql2000) +SELECT p.spid, NULLIF(CASE WHEN p.blocked = p.spid THEN 0 ELSE p.blocked END, 0), RTRIM(p.status), RTRIM(p.lastwaittype), + p.waittime, RTRIM(p.waitresource), DB_NAME(p.dbid), RTRIM(p.loginame), RTRIM(p.hostname), RTRIM(p.program_name), + p.open_tran, CAST(NULL AS nvarchar(4000)), p.last_batch, p.context_info +FROM master.dbo.sysprocesses p +WHERE (p.blocked > 0 AND p.blocked <> p.spid) + OR p.spid IN (SELECT blocked FROM master.dbo.sysprocesses WHERE blocked > 0 AND blocked <> spid) +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/_collectors/AxBatchCollector.sql b/SQL Scripts/_collectors/AxBatchCollector.sql new file mode 100644 index 0000000..c1076ce --- /dev/null +++ b/SQL Scripts/_collectors/AxBatchCollector.sql @@ -0,0 +1,137 @@ +/* + Delt collector-forespørgsel: Dynamics AX-batchjob (SQLI-AX-001, -002, -003, -004 og siden Dynamics AX → Batchjob) + + Hvad + Hvert 5. minut, i hver genkendt og overvåget AX-database: de nyeste 1000 batchjob (højeste RECID først) og – i AX + 2009 og senere – de afsluttede kørsler i BATCHJOBHISTORY efter seneste læste RECID (minus 2000, fordi AOS'er får + RecId i blokke). Axapta 3.0/4.0 har ingen historiktabel; der gemmer SQLInsight de kørsler, den selv ser blive + afsluttet. Kørslerne gemmes i SQLInsights repository (insight.AxBatchRuns, 90 dage) – aldrig i AX. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxBatchCollector.cs – AxBatchCollector.CollectAsync. + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – BatchJobs og BatchHistory. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): + Axapta 3.0: [bmssa].[BATCH]; AX 4.0: [dbo].[BATCH]; AX 2009/2012/F&O: [dbo].[BATCHJOB], [dbo].[BATCHJOBHISTORY]. + Versioner: SQL Server 2005 og nyere. Axapta 3.0: verificeret mod en kundes skema (STATUS-værdierne dog ikke). AX 4.0, + AX 2009, AX 2012 og F&O: baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, via [db].sys.sp_executesql. En låst tabel giver "ikke målt denne gang", ikke en fejl. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(b.[RECID] AS bigint), b.[CLASSNUM], CAST(NULL AS nvarchar(60)), b.[COMPANY], b.[GROUPID], b.[STATUS], b.[STARTDATE], b.[STARTTIME], CAST(NULL AS datetime), CAST(NULL AS int), b.[ENDDATE], b.[ENDTIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCH] b ORDER BY b.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: batchjob (AxSchemaAdapter.BatchJobs) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(j.[RECID] AS bigint), CAST(NULL AS int), j.[CAPTION], j.[COMPANY], CAST(NULL AS nvarchar(20)), j.[STATUS], j.[ORIGSTARTDATETIME], CAST(NULL AS int), j.[STARTDATETIME], CAST(NULL AS int), j.[ENDDATETIME], CAST(NULL AS int), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOB] j ORDER BY j.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h ORDER BY h.[RECID] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: kørselshistorik, de næste læsninger fra seneste RECID minus 2000 +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @FraRecId bigint; +SET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus 2000 +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (1000) CAST(h.[RECID] AS bigint), CAST(h.[BATCHJOBID] AS bigint), h.[CAPTION], h.[COMPANY], h.[STATUS], h.[STARTDATETIME], h.[ENDDATETIME], DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[BATCHJOBHISTORY] h WHERE h.[RECID] > 999999999 ORDER BY h.[RECID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +SET @Sql = REPLACE(@Sql, N'> 999999999', N'> ' + CAST(@FraRecId AS nvarchar(20))); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/_collectors/AxDatabaseDetector.sql b/SQL Scripts/_collectors/AxDatabaseDetector.sql new file mode 100644 index 0000000..8e36592 --- /dev/null +++ b/SQL Scripts/_collectors/AxDatabaseDetector.sql @@ -0,0 +1,120 @@ +/* + Delt collector-forespørgsel: Dynamics AX-genkendelsen (bruges af alle SQLI-AX- og COM-AX-regler, Servere og databaser + og Dynamics AX-siderne i SQLInsight Web) + + Hvad + Finder de overvågede databaser, der er Dynamics AX: SQLDICTIONARY og DATAAREA i samme skema. Versionen afgøres af + skemaet (src/SQLInsight.Core/DynamicsAx/AxDetection.cs – Recognise): + RECID på SQLDICTIONARY er int (præcision 10) → Axapta 2.5/3.0 (typisk skema bmssa) + bigint, BATCHJOB findes ikke → AX 4.0 + BATCHJOB findes, NUMBERSEQUENCESCOPE gør ikke → AX 2009 + NUMBERSEQUENCESCOPE, ingen PARTITION-kolonne → AX 2012 RTM/FP + PARTITION og modeldatabasen _model på instansen → AX 2012 R2/R3 + PARTITION uden noget modellager → Dynamics 365 F&O on-premises (formodet) + Læser kun metadata (sys.tables, sys.columns, HAS_PERMS_BY_NAME) og – hvis agenten må – firmalisten i DATAAREA. + Resultatet gemmes i hukommelsen i en time og som snapshot "dynamics-ax" i repository'et. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxDatabaseDetector.cs – AxDatabaseDetector.GetAsync (via + DynamicsAxCollector, hver time; også under Servere og databaser: src/SQLInsight.Collectors/Discovery/ + DatabaseDiscovery.cs – DynamicsAxSql). + Rettigheder: Genkendelsen: kun grant-least-privilege.sql (VIEW ANY DEFINITION, CONNECT ANY DATABASE). Firmalisten: + valgfri AX-læseadgang, GRANT SELECT ON [bmssa].[DATAAREA] TO []; (dbo efter AX 3.0). + Versioner: SQL Server 2005 og nyere; under Servere og databaser også SQL Server 2000 (sysobjects, uden rettigheds- + oplysninger). Axapta 3.0 er verificeret mod en kundes skema. AX 4.0, AX 2009, AX 2012 og F&O: baseret på + Microsofts dokumenterede skema – ikke verificeret mod kundedata. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Alle versioner: online brugerdatabaser og deres snapshot-indstillinger (AxDatabaseDetector.DatabasesSql) +SELECT name, snapshot_isolation_state, is_read_committed_snapshot_on +FROM sys.databases +WHERE database_id > 4 AND state = 0 AND source_database_id IS NULL AND HAS_DBACCESS(name) = 1 +ORDER BY name +GO + +-- Variant: Alle versioner: agentens login, til GRANT-teksten +SELECT SUSER_SNAME() +GO + +-- Variant: SQL Server 2005 og nyere: AX-genkendelse i hver overvåget database (AxDetection.MetadataSql via AxSql.InDatabase) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til databasen +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT s.name, UPPER(t.name), UPPER(c.name), TYPE_NAME(c.system_type_id), c.precision, c.max_length, + ISNULL(HAS_PERMS_BY_NAME(QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name), N''OBJECT'', N''SELECT''), 0), + (SELECT TOP 1 p.name FROM sys.database_principals p WHERE p.sid = SUSER_SID()) +FROM sys.tables t +JOIN sys.schemas s ON s.schema_id = t.schema_id +JOIN sys.columns c ON c.object_id = t.object_id +WHERE UPPER(t.name) IN (N''SQLDICTIONARY'', N''DATAAREA'', N''SYSCONFIG'', N''BATCH'', N''BATCHJOB'', N''BATCHJOBHISTORY'', N''BATCHGROUP'', N''NUMBERSEQUENCETABLE'', N''NUMBERSEQUENCESCOPE'', N''NUMBERSEQUENCEREFERENCE'', N''SYSTEMSEQUENCES'', N''MODELELEMENT'') +ORDER BY s.name, t.name, c.column_id'; +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: SQL Server 2000 (kun opdagelse under Servere og databaser, AxDetection.MetadataSql2000) +SELECT u.name, UPPER(o.name), UPPER(c.name), TYPE_NAME(c.xtype), c.xprec, c.length, 0, NULL +FROM [AXDB].dbo.sysobjects o +JOIN [AXDB].dbo.sysusers u ON u.uid = o.uid +JOIN [AXDB].dbo.syscolumns c ON c.id = o.id +WHERE o.xtype = 'U' AND UPPER(o.name) IN (N'SQLDICTIONARY', N'DATAAREA', N'SYSCONFIG', N'BATCH', N'BATCHJOB', N'BATCHJOBHISTORY', N'BATCHGROUP', N'NUMBERSEQUENCETABLE', N'NUMBERSEQUENCESCOPE', N'NUMBERSEQUENCEREFERENCE', N'SYSTEMSEQUENCES', N'MODELELEMENT') +ORDER BY u.name, o.name, c.colid +GO + +-- Variant: Axapta 3.0 (skema bmssa): firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [bmssa].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: firmaer (AxSchemaAdapter.Companies) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) d.[ID], d.[NAME], d.[ISVIRTUAL] FROM [dbo].[DATAAREA] d ORDER BY d.[ID]'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/SQL Scripts/_collectors/AxSequenceCollector.sql b/SQL Scripts/_collectors/AxSequenceCollector.sql new file mode 100644 index 0000000..6230e48 --- /dev/null +++ b/SQL Scripts/_collectors/AxSequenceCollector.sql @@ -0,0 +1,133 @@ +/* + Delt collector-forespørgsel: Dynamics AX-nummerserier og RecId (SQLI-AX-005, -006, -007, -008, -009 og siderne + Dynamics AX → Nummerserier og RecId) + + Hvad + Hver time, i hver genkendt og overvåget AX-database: de 5000 mest brugte nummerserier (NUMBERSEQUENCETABLE, med + firma og om en reference bruger serien) og de 500 højeste RecId-serier (SYSTEMSEQUENCES med NAME = 'SEQNO'). + SQLInsight gemmer én måling pr. serie pr. dag af NEXTREC/NEXTVAL i sit eget repository (insight.AxSequenceSamples, + 400 dage) og laver prognoserne ud fra den – aldrig i AX. + + Collector: src/SQLInsight.Collectors/DynamicsAx/AxSequenceCollector.cs – AxSequenceCollector.CollectAsync. + SQL: src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs – NumberSequences og RecIds. + Rettigheder: Valgfri AX-læseadgang, kun SELECT (db/permissions/grant-dynamics-ax-read.sql): NUMBERSEQUENCETABLE, + NUMBERSEQUENCEREFERENCE, NUMBERSEQUENCESCOPE (AX 2012/F&O), SYSTEMSEQUENCES og SQLDICTIONARY (tabelnavne), + i [bmssa] i Axapta 3.0 og [dbo] senere. + Versioner: SQL Server 2005 og nyere. Axapta 3.0: verificeret mod en kundes skema. AX 4.0, AX 2009, AX 2012 og F&O: + baseret på Microsofts dokumenterede skema – ikke verificeret mod kundedata. + Læsning: Kun SELECT med TOP, READ COMMITTED med SET LOCK_TIMEOUT 2000 (SNAPSHOT, når databasen tillader det), aldrig + NOLOCK, via [db].sys.sp_executesql. +*/ +-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden. +-- Variant: Axapta 3.0 (skema bmssa): nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [bmssa].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [bmssa].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATE], n.[LATESTCLEANTIME], CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], n.[DATAAREAID], n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCE] = n.[NUMBERSEQUENCE] AND r.[DATAAREAID] = n.[DATAAREAID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: nummerserier (AxSchemaAdapter.NumberSequences) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (5000) CAST(n.[RECID] AS bigint), n.[NUMBERSEQUENCE], NULLIF(sc.[DATAAREA], N''''), n.[TXT], CAST(n.[LOWEST] AS bigint), CAST(n.[HIGHEST] AS bigint), CAST(n.[NEXTREC] AS bigint), n.[FORMAT], ISNULL(n.[BLOCKED], 0), ISNULL(n.[CONTINUOUS], 0), ISNULL(n.[CYCLIC], 0), ISNULL(n.[MANUAL], 0), CASE WHEN EXISTS (SELECT 1 FROM [dbo].[NUMBERSEQUENCEREFERENCE] r WHERE r.[NUMBERSEQUENCEID] = n.[RECID]) THEN 1 ELSE 0 END, n.[LATESTCLEANDATETIME], CAST(NULL AS int), CAST(n.[CLEANINTERVAL] AS float), DATEDIFF(minute, GETDATE(), GETUTCDATE()) FROM [dbo].[NUMBERSEQUENCETABLE] n LEFT JOIN [dbo].[NUMBERSEQUENCESCOPE] sc ON sc.[RECID] = n.[NUMBERSEQUENCESCOPE] ORDER BY (CAST(n.[NEXTREC] AS float) - n.[LOWEST]) / NULLIF(CAST(n.[HIGHEST] AS float) - n.[LOWEST], 0) DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: Axapta 3.0 (skema bmssa): RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'bmssa'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [bmssa].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [bmssa].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[bmssa].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 4.0 (skema dbo) – ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO + +-- Variant: AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata: RecId pr. tabel (AxSchemaAdapter.RecIds) +DECLARE @Database sysname; +SET @Database = N'AXDB'; -- <== ret til AX-databasen +DECLARE @Schema sysname; +SET @Schema = N'dbo'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo +DECLARE @Sql nvarchar(max); +SET @Sql = N'SET NOCOUNT ON; SET LOCK_TIMEOUT 2000; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT TOP (500) s.[TABID], s.[DATAAREAID], CAST(s.[NEXTVAL] AS bigint), CAST(s.[MAXVAL] AS bigint), (SELECT TOP 1 d.[NAME] FROM [dbo].[SQLDICTIONARY] d WHERE d.[TABLEID] = s.[TABID] AND d.[FIELDID] = 0) FROM [dbo].[SYSTEMSEQUENCES] s WHERE s.[NAME] = N''SEQNO'' ORDER BY s.[NEXTVAL] DESC'; +SET @Sql = REPLACE(@Sql, N'[dbo].', QUOTENAME(@Schema) + N'.'); +DECLARE @Procedure nvarchar(300); +SET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql'; +EXEC @Procedure @Sql; +SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED; +GO +-- <<< slut på genereret SQL diff --git a/src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs b/src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs index 8930916..15631b2 100644 --- a/src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs +++ b/src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs @@ -298,6 +298,7 @@ public abstract class AxSchemaAdapter Manual = row.Manual, Referenced = row.Referenced, LastCleanUtc = lastClean, + CleanupKnown = row.LastCleanDate is not null, CleanIntervalHours = row.CleanIntervalHours, Forecast = row.Manual || row.Cyclic ? null : AxForecast.From(samples, nowUtc, row.Next, max, forecastDays), }; diff --git a/src/SQLInsight.Core/DynamicsAx/AxSequences.cs b/src/SQLInsight.Core/DynamicsAx/AxSequences.cs index eda3695..a41f186 100644 --- a/src/SQLInsight.Core/DynamicsAx/AxSequences.cs +++ b/src/SQLInsight.Core/DynamicsAx/AxSequences.cs @@ -55,6 +55,9 @@ public sealed record AxNumberSequence public bool Manual { get; init; } public bool? Referenced { get; init; } public DateTime? LastCleanUtc { get; init; } + + /// The last cleanup is known (the column exists); null then means never cleaned up. + public bool CleanupKnown { get; init; } public double? CleanIntervalHours { get; init; } public AxForecast? Forecast { get; init; } diff --git a/src/SQLInsight.Core/DynamicsAx/AxSql.cs b/src/SQLInsight.Core/DynamicsAx/AxSql.cs index 0f8f384..b2e7212 100644 --- a/src/SQLInsight.Core/DynamicsAx/AxSql.cs +++ b/src/SQLInsight.Core/DynamicsAx/AxSql.cs @@ -38,16 +38,22 @@ public static class AxSql /// the isolation and lock timeout for AX reads. The SET options only apply inside sp_executesql; they are reset /// afterwards anyway. /// - public static string InDatabase(string database, string innerSql, AxReadIsolation isolation = AxReadIsolation.ReadCommitted) + public static string InDatabase(string database, string innerSql, AxReadIsolation isolation = AxReadIsolation.ReadCommitted) => + "EXEC " + Identifier(database) + ".sys.sp_executesql " + Literal(Batch(innerSql, isolation)) + ";\n" + Reset; + + /// The batch sp_executesql runs inside the AX database: lock timeout, isolation, then the query. + public static string Batch(string innerSql, AxReadIsolation isolation = AxReadIsolation.ReadCommitted) { ArgumentNullException.ThrowIfNull(innerSql); - var inner = new StringBuilder() + return new StringBuilder() .Append("SET NOCOUNT ON; SET LOCK_TIMEOUT ").Append(LockTimeoutMilliseconds.ToString(CultureInfo.InvariantCulture)).Append("; ") .Append(isolation == AxReadIsolation.Snapshot ? "SET TRANSACTION ISOLATION LEVEL SNAPSHOT; " : "SET TRANSACTION ISOLATION LEVEL READ COMMITTED; ") .Append(innerSql) .ToString(); - return "EXEC " + Identifier(database) + ".sys.sp_executesql " + Literal(inner) + ";\nSET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;"; } + + /// Sent after every AX read, so the session's settings are the defaults again for the next collector. + public const string Reset = "SET LOCK_TIMEOUT -1; SET TRANSACTION ISOLATION LEVEL READ COMMITTED;"; } /// diff --git a/src/SQLInsight.Rules/BuiltInRules.cs b/src/SQLInsight.Rules/BuiltInRules.cs index 8ce40e4..201bfdd 100644 --- a/src/SQLInsight.Rules/BuiltInRules.cs +++ b/src/SQLInsight.Rules/BuiltInRules.cs @@ -2,6 +2,7 @@ using SQLInsight.Rules.Availability; using SQLInsight.Rules.Concurrency; using SQLInsight.Rules.Configuration; using SQLInsight.Rules.Databases; +using SQLInsight.Rules.DynamicsAx; using SQLInsight.Rules.Engine; using SQLInsight.Rules.Indexes; using SQLInsight.Rules.Instance; @@ -101,6 +102,18 @@ public static class BuiltInRules // Concurrency new BlockingRule(), new DeadlockRule(), + + // Dynamics AX (only in databases recognised as AX and chosen for monitoring) + new AxBatchFailedRule(), + new AxBatchLongRunningRule(), + new AxBatchLateRule(), + new AxBatchStuckRule(), + new AxNumberSequenceExhaustionRule(), + new AxNumberSequenceFormatRule(), + new AxNumberSequenceBlockedRule(), + new AxNumberSequenceContinuousRule(), + new AxRecIdExhaustionRule(), + new AxBlockingUserRule(), ]; } } diff --git a/src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs b/src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs new file mode 100644 index 0000000..8e70137 --- /dev/null +++ b/src/SQLInsight.Rules/DynamicsAx/DynamicsAxRules.cs @@ -0,0 +1,508 @@ +using System.Globalization; +using SQLInsight.Core.Configuration; +using SQLInsight.Core.Data; +using SQLInsight.Core.DynamicsAx; +using SQLInsight.Core.Findings; +using SQLInsight.Core.Instances; +using SQLInsight.Rules.Engine; + +namespace SQLInsight.Rules.DynamicsAx; + +/// +/// Shared by the Dynamics AX rules: they only look at databases recognised as AX and chosen for monitoring (the +/// collectors only read those), report "ikke relevant" without any, and "ikke målbar" with the exact reason (often the +/// GRANT text for the optional read access) when no AX database could be read. Findings are about one database +/// (subject "database: X · …"), so they only reach SQLInsight Hub where the AX opt-in is ticked. +/// +public abstract class DynamicsAxRule : Rule +{ + /// Most findings per database and rule, so one broken AX installation cannot flood the list. + protected const int MaxFindingsPerDatabase = 25; + + public override IReadOnlyCollection RequiredCapabilities => [Capability.DynamicManagementViews]; + + protected const string NoAxDatabases = "Ikke relevant: ingen af de overvågede databaser på instansen er genkendt som Dynamics AX."; + + /// The databases that could be measured, or the outcome when none could. + protected static (IReadOnlyList Measured, RuleEvaluation? Outcome) Measured(IReadOnlyList databases, Func name, Func notMeasurable) + { + if (databases.Count == 0) + { + return ([], RuleEvaluation.NotApplicable(NoAxDatabases)); + } + + var measured = databases.Where(d => notMeasurable(d) is null).ToList(); + return measured.Count > 0 + ? (measured, null) + : ([], RuleEvaluation.NotMeasurable(string.Join(" ", databases.Select(d => $"{name(d)}: {notMeasurable(d)}")))); + } + + protected static string Time(DateTime? utc) => utc is { } t ? t.ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture) : "–"; + + protected static string Duration(TimeSpan span) => span.TotalHours >= 2 + ? $"{Format.Number(span.TotalHours, 1)} timer" + : $"{Format.Number(span.TotalMinutes)} minutter"; + + protected static string Seconds(double? seconds) => seconds is { } s ? Duration(TimeSpan.FromSeconds(s)) : "–"; + + protected static Evidence Version(AxVersion version) => new("AX-version", AxVersions.Text(version) + (AxVersions.IsVerified(version) ? string.Empty : " – " + AxVersions.VerificationText(version))); + +} + +/// Batch rules: read . +public abstract class DynamicsAxBatchRule : DynamicsAxRule +{ + public override RuleEvaluation Evaluate(RuleContext context) + { + ArgumentNullException.ThrowIfNull(context); + var snapshot = context.Data.Get(); + var (measured, outcome) = Measured(snapshot.Databases, d => d.Database, d => d.NotMeasurable); + return outcome ?? Result(measured.SelectMany(d => Check(d, snapshot.Thresholds, context.Now).Take(MaxFindingsPerDatabase))); + } + + protected abstract IEnumerable Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now); + + protected Finding JobFinding(AxBatchDatabase database, AxBatchJob job, Severity severity, string title, string explanation, string recommendation, + IEnumerable evidence) => + CreateFinding(severity, title, explanation, recommendation, + [ + new Evidence("Database", database.Database), + new Evidence("Batchjob", job.Caption), + new Evidence("Firma", job.Company ?? "–"), + new Evidence("Status i AX", $"{StatusText(job.Status)} (STATUS = {job.StatusCode.ToString(CultureInfo.InvariantCulture)})"), + .. evidence, + Version(database.Version), + ], + DynamicsAxFindings.Subject(database.Database, "batch: " + job.Key)); + + public static string StatusText(AxBatchStatus status) => status switch + { + AxBatchStatus.Hold => "Tilbageholdt", + AxBatchStatus.Waiting => "Venter", + AxBatchStatus.Executing => "Udfører", + AxBatchStatus.Error => "Fejl", + AxBatchStatus.Finished => "Afsluttet", + AxBatchStatus.Ready => "Klar", + AxBatchStatus.NotRun => "Ikke kørt", + AxBatchStatus.Cancelling => "Annullerer", + AxBatchStatus.Canceled => "Annulleret", + _ => "Ukendt", + }; +} + +/// A batch job ended in error, now or in a run within the lookback (a recurring job is already waiting for its next run). +public sealed class AxBatchFailedRule : DynamicsAxBatchRule +{ + public override string Id => "SQLI-AX-001"; + public override string Title => "Dynamics AX-batchjob er endt med fejl"; + + protected override IEnumerable Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + var since = now.UtcDateTime.AddHours(-thresholds.BatchFailedLookbackHours); + foreach (var job in database.Jobs.Where(j => j.Status == AxBatchStatus.Error && (j.EndedUtc ?? j.PlannedStartUtc ?? now.UtcDateTime) >= since + || j.Status != AxBatchStatus.Error && j.LastRunStatus == AxBatchStatus.Error && j.LastRunEndedUtc >= since) + .OrderByDescending(j => j.EndedUtc ?? j.LastRunEndedUtc)) + { + var failedAt = job.Status == AxBatchStatus.Error ? job.EndedUtc : job.LastRunEndedUtc; + yield return JobFinding(database, job, Severity.High, + $"Batchjobbet \"{job.Caption}\" i {database.Database} er endt med fejl", + "Et batchjob i Dynamics AX er stoppet med fejl. Det, jobbet skulle gøre (fx bogføring, lagerafslutning, integration eller afsendelse), " + + "er ikke gjort eller kun delvist gjort, og gentagne job fortsætter ikke nødvendigvis af sig selv.", + "Åbn batchjobbet i AX (Basis → Forespørgsler → Batchjob) og læs loggen/infologgen for fejlen. Ret årsagen, og sæt jobbet til Venter igen " + + "eller kør det manuelt. Gentager fejlen sig, så kontakt AX-partneren med loggen.", + [new Evidence("Fejlede", Time(failedAt)), new Evidence("Planlagt start", Time(job.PlannedStartUtc))]); + } + } +} + +/// A running batch job has run much longer than its own history says is normal. +public sealed class AxBatchLongRunningRule : DynamicsAxBatchRule +{ + public override string Id => "SQLI-AX-002"; + public override string Title => "Dynamics AX-batchjob kører længere end normalt"; + + protected override IEnumerable Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var job in database.Jobs.Where(j => j.Status == AxBatchStatus.Executing && j.HistoryRuns >= thresholds.BatchHistoryMinimumRuns && j.P90Seconds is not null)) + { + var running = job.Running(now)!.Value; + var limit = TimeSpan.FromSeconds(Math.Max(job.P90Seconds!.Value * thresholds.BatchLongRunningFactor, thresholds.BatchLongRunningMinimumMinutes * 60.0)); + if (running < limit) + { + continue; + } + + yield return JobFinding(database, job, running >= limit * 2 ? Severity.High : Severity.Medium, + $"Batchjobbet \"{job.Caption}\" i {database.Database} har kørt i {Duration(running)} – normalt {Seconds(job.MedianSeconds)}", + $"Jobbet har kørt mere end {Format.Number(thresholds.BatchLongRunningFactor, 1)} gange så længe som 90 % af dets egne seneste {job.HistoryRuns} kørsler. " + + "Det kan skyldes blokering, en ændret datamængde, en dårlig forespørgselsplan eller at jobbet hænger. Imens kan det holde låse, som brugerne venter på.", + "Se under Analyse af tidsrum og Blokering, hvad sessionen venter på lige nu. Sammenlign med datamængden (fx antal posteringer) i perioden. " + + "Er jobbet gået i stå, så annullér det i AX og kør det igen uden for åbningstiden.", + [ + new Evidence("Kører siden", Time(job.RunningSince)), + new Evidence("Median", Seconds(job.MedianSeconds)), + new Evidence("90 %-fraktil", Seconds(job.P90Seconds)), + new Evidence("Kørsler i historikken", job.HistoryRuns.ToString(CultureInfo.InvariantCulture)), + .. job.StartedUtc is null + ? [new Evidence("Bemærk", "AX gemmer ikke det faktiske starttidspunkt i denne version; SQLInsight måler fra første gang, den så jobbet køre.")] + : Array.Empty(), + ]); + } + } +} + +/// A waiting batch job's planned start passed long ago: the batch server does not pick it up. +public sealed class AxBatchLateRule : DynamicsAxBatchRule +{ + public override string Id => "SQLI-AX-003"; + public override string Title => "Dynamics AX-batchjob er ikke startet til tiden"; + + protected override IEnumerable Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + var late = database.Jobs.Where(j => j.Late(now) is { } l && l >= TimeSpan.FromMinutes(thresholds.BatchLateMinutes)).OrderByDescending(j => j.Late(now)).ToList(); + foreach (var job in late) + { + var behind = job.Late(now)!.Value; + var never = behind >= TimeSpan.FromHours(thresholds.BatchNeverStartedHours); + yield return JobFinding(database, job, never ? Severity.High : Severity.Medium, + never + ? $"Batchjobbet \"{job.Caption}\" i {database.Database} er aldrig startet ({Duration(behind)} over planlagt start)" + : $"Batchjobbet \"{job.Caption}\" i {database.Database} er {Duration(behind)} forsinket", + "Jobbet venter stadig, selv om dets planlagte start er passeret. Batchserveren (AOS med batch, eller batch-klienten i AX 3.0) tager det ikke: " + + "den kører ikke, er optaget af andre job, eller betjener ikke jobbets batchgruppe." + + (late.Count >= 5 ? $" {late.Count} job i databasen er forsinkede, hvilket typisk betyder, at batchserveren slet ikke kører." : string.Empty), + "Tjek at batchserveren kører (AX 3.0: batch-klienten er startet og har gruppen; AX 2009/2012: AOS-instansen er sat op som batchserver " + + "med gruppen under Batchservergrupper), og at antallet af samtidige batchtråde ikke er brugt op.", + [ + new Evidence("Planlagt start", Time(job.PlannedStartUtc)), + new Evidence("Forsinkelse", Duration(behind)), + new Evidence("Batchgruppe", job.Group ?? "–"), + .. AxVersions.HasUtcBatchTimes(database.Version) + ? Array.Empty() + : [new Evidence("Tid", "AX gemmer lokal dato + sekunder efter midnat; omregnet til UTC med SQL Servers tidszone.")], + ]); + } + } +} + +/// A batch job has been executing for hours, longer than it ever runs: it is stuck. +public sealed class AxBatchStuckRule : DynamicsAxBatchRule +{ + public override string Id => "SQLI-AX-004"; + public override string Title => "Dynamics AX-batchjob hænger i Udfører"; + + protected override IEnumerable Check(AxBatchDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var job in database.Jobs.Where(j => j.Status == AxBatchStatus.Executing)) + { + var running = job.Running(now)!.Value; + var normallyLong = job.P90Seconds is { } p90 && running.TotalSeconds <= p90 * thresholds.BatchLongRunningFactor; + if (running < TimeSpan.FromHours(thresholds.BatchStuckHours) || normallyLong) + { + continue; + } + + yield return JobFinding(database, job, Severity.High, + $"Batchjobbet \"{job.Caption}\" i {database.Database} har stået i Udfører i {Duration(running)}", + "Jobbet har haft status Udfører i flere timer. Typisk er AOS'en eller batch-klienten genstartet eller gået ned midt i kørslen, så jobbet aldrig " + + "blev afsluttet: det står som kørende, men intet sker, og gentagne job planlægges ikke igen. Det kan også være gået i stå på en lås.", + "Tjek under Blokering, om der er en session for jobbet. Er der ikke, så sæt jobbet til Fejl/Venter i AX (eller annullér det) og kør det igen. " + + "Find ud af, hvorfor batchserveren stoppede (Windows-hændelseslog på AOS-serveren).", + [ + new Evidence("Kører siden", Time(job.RunningSince)), + new Evidence("Normal varighed (median)", Seconds(job.MedianSeconds)), + .. job.StartedUtc is null + ? [new Evidence("Bemærk", "Målt fra første gang SQLInsight så jobbet køre; det kan have kørt længere.")] + : Array.Empty(), + ]); + } + } +} + +/// Number sequence rules: read . +public abstract class DynamicsAxNumberSequenceRule : DynamicsAxRule +{ + public override RuleEvaluation Evaluate(RuleContext context) + { + ArgumentNullException.ThrowIfNull(context); + var snapshot = context.Data.Get(); + var (measured, outcome) = Measured(snapshot.Databases, d => d.Database, d => d.NumberSequencesNotMeasurable); + return outcome ?? Result(measured.SelectMany(d => Check(d, snapshot.Thresholds, context.Now).Take(MaxFindingsPerDatabase))); + } + + protected abstract IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now); + + protected Finding SequenceFinding(AxSequenceDatabase database, AxNumberSequence sequence, Severity severity, string title, string explanation, string recommendation, + IEnumerable? evidence = null) => + CreateFinding(severity, title, explanation, recommendation, + [ + new Evidence("Database", database.Database), + new Evidence("Firma", sequence.Company ?? "delt/ukendt"), + new Evidence("Nummerserie", sequence.Code + (sequence.Text is { } text ? $" ({text})" : string.Empty)), + new Evidence("Næste nummer (NEXTREC)", Format.Number(sequence.Next)), + new Evidence("Højeste (HIGHEST)", Format.Number(sequence.Highest)), + new Evidence("Format", sequence.Format ?? "–"), + .. evidence ?? [], + Version(database.Version), + ], + DynamicsAxFindings.Subject(database.Database, "nummerserie: " + sequence.Key)); + + protected static string Name(AxNumberSequence sequence) => sequence.Company is { } company ? $"{sequence.Code} ({company})" : sequence.Code; +} + +/// A number sequence is nearly used up, now or within the forecast. +public sealed class AxNumberSequenceExhaustionRule : DynamicsAxNumberSequenceRule +{ + public override string Id => "SQLI-AX-005"; + public override string Title => "Dynamics AX-nummerserie er ved at løbe tør"; + + protected override IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var s in database.NumberSequences + .Where(s => !s.Manual && !s.Cyclic && !s.Blocked) + .Where(s => s.UsedPercent >= thresholds.NumberSequenceWarnPercent || s.Forecast?.DaysLeft <= thresholds.ForecastWarnDays) + .OrderBy(s => s.Remaining == 0 ? 0 : 1).ThenBy(s => s.Forecast?.DaysLeft ?? double.MaxValue).ThenByDescending(s => s.UsedPercent)) + { + var severity = s.Remaining == 0 ? Severity.Critical + : s.UsedPercent >= thresholds.NumberSequenceCriticalPercent || s.Forecast?.DaysLeft <= thresholds.ForecastCriticalDays ? Severity.High + : Severity.Medium; + yield return SequenceFinding(database, s, severity, + s.Remaining == 0 + ? $"Nummerserien {Name(s)} i {database.Database} er opbrugt" + : s.Forecast?.DaysLeft is { } days && days <= thresholds.ForecastWarnDays + ? $"Nummerserien {Name(s)} i {database.Database} løber tør om ca. {Format.Number(days)} dage" + : $"Nummerserien {Name(s)} i {database.Database} er {Format.Number(s.UsedPercent, 1)} % brugt", + "Når en nummerserie er brugt op, kan AX ikke tildele flere numre: oprettelse eller bogføring af fx ordrer, fakturaer eller posteringer, " + + "der bruger serien, fejler for alle brugere. Prognosen bygger på SQLInsights egne daglige målinger af NEXTREC.", + "Hæv Højeste i nummerserien i AX (og udvid formatet med flere #, hvis numrene ikke kan være der), eller opret en ny serie og skift " + + "referencen til den, før serien løber tør. Gør det uden for travle perioder, og aftal det med regnskab, hvis numrene skal være fortløbende.", + [ + new Evidence("Brugt", $"{Format.Number(s.UsedPercent, 1)} %"), + new Evidence("Numre tilbage", Format.Number(s.Remaining)), + new Evidence("Forbrug pr. dag", s.Forecast is { PerDay: > 0 } f ? $"{Format.Number(f.PerDay, 1)} (målt over {Format.Number(f.BasisDays, 1)} dage)" : "ukendt endnu"), + new Evidence("Løber tør om", s.Forecast?.DaysLeft is { } left ? $"{Format.Number(left)} dage" : "–"), + ]); + } + } +} + +/// The format has fewer digits than HIGHEST needs: numbers above the format's capacity cannot be shown. +public sealed class AxNumberSequenceFormatRule : DynamicsAxNumberSequenceRule +{ + public override string Id => "SQLI-AX-006"; + public override string Title => "Dynamics AX-nummerseriens format er for kort til Højeste"; + + protected override IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var s in database.NumberSequences.Where(s => s.FormatTooShort && !s.Manual).OrderByDescending(s => s.Next)) + { + var overflowing = s.Next > s.FormatCapacity; + yield return SequenceFinding(database, s, overflowing ? Severity.High : Severity.Medium, + overflowing + ? $"Numrene i nummerserien {Name(s)} i {database.Database} kan ikke længere være i formatet {s.Format}" + : $"Formatet {s.Format} for nummerserien {Name(s)} i {database.Database} har plads til færre numre end Højeste", + $"Formatet har {s.FormatDigits} cifre (#), så det største nummer, der kan vises, er {Format.Number(s.FormatCapacity ?? 0)}, men Højeste er " + + $"{Format.Number(s.Highest)}. Når serien når forbi {Format.Number(s.FormatCapacity ?? 0)}, fejler tildelingen af numre, selv om serien ikke er brugt op.", + "Udvid formatet med flere #, eller sæt Højeste ned til formatets kapacitet, så serien opfører sig, som den ser ud. Tjek, at felterne, " + + "numrene gemmes i, er lange nok til det længere format.", + [new Evidence("Formatets kapacitet", Format.Number(s.FormatCapacity ?? 0)), new Evidence("Brugt af formatet", $"{Format.Number(s.UsedPercent, 1)} %")]); + } + } +} + +/// A blocked number sequence is still referenced: whatever uses it fails. +public sealed class AxNumberSequenceBlockedRule : DynamicsAxNumberSequenceRule +{ + public override string Id => "SQLI-AX-007"; + public override string Title => "Spærret Dynamics AX-nummerserie bruges stadig"; + + protected override IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now) => + database.NumberSequences.Where(s => s.Blocked && s.Referenced == true).OrderBy(s => s.Key, StringComparer.Ordinal).Select(s => + SequenceFinding(database, s, Severity.Medium, + $"Nummerserien {Name(s)} i {database.Database} er spærret, men bruges stadig", + "Serien er spærret (Spærret), men en nummerseriereference peger stadig på den. Alt i AX, der skal have et nummer fra den reference, fejler " + + "med en fejl om, at nummerserien er spærret.", + "Find referencen under Nummerserier → Referencer (eller i modulets parametre under Nummerserier) og skift den til en aktiv serie, " + + "eller fjern spærringen, hvis den er sat ved en fejl.")); +} + +/// A continuous number sequence's unused numbers have not been cleaned up for a long time. +public sealed class AxNumberSequenceContinuousRule : DynamicsAxNumberSequenceRule +{ + /// Cleanup older than this is a finding, unless the sequence's own interval is longer (then twice that). + public const int MinimumDays = 7; + + public override string Id => "SQLI-AX-008"; + public override string Title => "Kontinuerlig Dynamics AX-nummerserie er ikke ryddet op"; + + protected override IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var s in database.NumberSequences.Where(s => s.Continuous && !s.Blocked && !s.Manual && s.CleanupKnown && s.Next > s.Lowest)) + { + var limit = TimeSpan.FromDays(Math.Max(MinimumDays, 2 * (s.CleanIntervalHours ?? 0) / 24)); + var age = s.LastCleanUtc is { } last ? now.UtcDateTime - last : (TimeSpan?)null; + if (age is { } a && a < limit) + { + continue; + } + + yield return SequenceFinding(database, s, Severity.Low, + age is null + ? $"Den kontinuerlige nummerserie {Name(s)} i {database.Database} er aldrig ryddet op" + : $"Den kontinuerlige nummerserie {Name(s)} i {database.Database} er ikke ryddet op i {Format.Number(age.Value.TotalDays)} dage", + "En kontinuerlig nummerserie må ikke have huller. Numre, der er reserveret af en session, som blev afbrudt (fx en klient, der gik ned), " + + "bliver liggende som optagede, til serien ryddes op. Uden oprydning opstår huller og fejl om optagede numre, og serien bliver en flaskehals, " + + "fordi hver tildeling låser rækken i NUMBERSEQUENCETABLE til transaktionen er færdig.", + "Kør Oprydning på nummerserien (Nummerserier → Funktioner → Oprydning) eller slå automatisk oprydning til med et passende interval. " + + "Overvej om serien virkelig skal være kontinuerlig: ikke-kontinuerlige serier låser ikke.", + [ + new Evidence("Seneste oprydning", age is null ? "aldrig" : Time(s.LastCleanUtc)), + new Evidence("Oprydningsinterval", s.CleanIntervalHours is > 0 and var h ? $"{Format.Number(h, 1)} timer" : "ikke sat"), + ]); + } + } +} + +/// A table's RecId sequence is approaching its limit: 32-bit in AX 3.0, so flagged much earlier there. +public sealed class AxRecIdExhaustionRule : DynamicsAxRule +{ + public override string Id => "SQLI-AX-009"; + public override string Title => "Dynamics AX RecId er ved at løbe tør"; + + public override RuleEvaluation Evaluate(RuleContext context) + { + ArgumentNullException.ThrowIfNull(context); + var snapshot = context.Data.Get(); + var (measured, outcome) = Measured(snapshot.Databases, d => d.Database, d => d.RecIdsNotMeasurable); + return outcome ?? Result(measured.SelectMany(d => Check(d, snapshot.Thresholds, context.Now).Take(MaxFindingsPerDatabase))); + } + + /// The severity for a RecId sequence, or null when it is fine. + public static Severity? Assess(AxRecIdSequence r, AxThresholds t) + { + ArgumentNullException.ThrowIfNull(r); + ArgumentNullException.ThrowIfNull(t); + if (r.Remaining == 0) + { + return Severity.Critical; + } + + var days = r.Forecast?.DaysLeft; + if (r.Bits == 32) + { + return r.UsedPercent >= t.RecId32CriticalPercent || days <= t.RecId32ForecastDays / 2.0 ? Severity.Critical + : r.UsedPercent >= t.RecId32WarnPercent || days <= t.RecId32ForecastDays ? Severity.High + : null; + } + + return r.UsedPercent >= t.RecId64WarnPercent || days <= t.ForecastWarnDays ? Severity.High : null; + } + + private IEnumerable Check(AxSequenceDatabase database, AxThresholds thresholds, DateTimeOffset now) + { + foreach (var (r, severity) in database.RecIds.Select(r => (r, Assess(r, thresholds))).Where(x => x.Item2 is not null).OrderByDescending(x => x.r.UsedPercent)) + { + var table = r.TableName ?? $"tabel-id {r.TableId.ToString(CultureInfo.InvariantCulture)}"; + yield return CreateFinding(severity!.Value, + $"RecId for {table}{(r.Company is { } c ? $" ({c})" : string.Empty)} i {database.Database} er {Format.Number(r.UsedPercent, 1)} % brugt", + r.Bits == 32 + ? "AX 3.0 bruger 32-bit RecId (int): højst 2.147.483.647 pr. tabel/firma. Når RecId er brugt op, kan AX ikke oprette flere poster, " + + "og hele installationen står stille. Det kan ikke løses ved at hæve en grænse, kun ved et større oprydnings- eller omnummereringsprojekt, " + + "som tager tid at planlægge – derfor advares tidligt." + : "RecId-serien nærmer sig sin grænse (MAXVAL i SYSTEMSEQUENCES). Når den er nået, kan AX ikke oprette flere poster i tabellen.", + r.Bits == 32 + ? "Kontakt AX-partneren og planlæg en løsning nu: oprydning af gamle data, omnummerering af RecId (særligt værktøj) eller opgradering " + + "til en AX-version med 64-bit RecId. Følg forbruget her: prognosen bygger på SQLInsights egne daglige målinger af NEXTVAL." + : "Undersøg hvorfor serien bruges så hurtigt, eller hvorfor MAXVAL er sat lavt, sammen med AX-partneren.", + [ + new Evidence("Database", database.Database), + new Evidence("Tabel", table), + new Evidence("Firma", r.Company ?? "–"), + new Evidence("Næste RecId (NEXTVAL)", Format.Number(r.Next)), + new Evidence("Grænse", Format.Number(r.Max) + (r.Bits == 32 ? " (32-bit)" : string.Empty)), + new Evidence("Forbrug pr. dag", r.Forecast is { PerDay: > 0 } f ? Format.Number(f.PerDay) : "ukendt endnu"), + new Evidence("Løber tør om", r.Forecast?.DaysLeft is { } d ? $"{Format.Number(d)} dage" : "–"), + Version(database.Version), + ], + DynamicsAxFindings.Subject(database.Database, "recid: " + r.Key)); + } + } +} + +/// +/// An AX session blocks others: who in AX is behind it, as far as SQL Server can tell (CONTEXT_INFO from the AOS in +/// AX 2009/2012; never in AX 3.0/4.0, which the finding says). +/// +public sealed class AxBlockingUserRule : DynamicsAxRule +{ + public override string Id => "SQLI-AX-010"; + public override string Title => "AX-bruger blokerer andre"; + + public override RuleEvaluation Evaluate(RuleContext context) + { + ArgumentNullException.ThrowIfNull(context); + var ax = context.Data.Get(); + if (ax.Databases.Count == 0) + { + return RuleEvaluation.NotApplicable(NoAxDatabases); + } + + var blocking = context.Data.Get(); + var thresholds = context.Data.TryGet(out var batch) ? batch.Thresholds : new AxThresholds(); + var findings = new List(); + foreach (var head in blocking.HeadBlockers) + { + var victims = Chain(blocking, head.SessionId); + var longest = victims.Count == 0 ? 0 : victims.Max(v => v.WaitTimeMs ?? 0); + var database = ax.Find(head.DatabaseName) ?? victims.Select(v => ax.Find(v.DatabaseName)).FirstOrDefault(d => d is not null); + if (database is null || longest < thresholds.BlockingSeconds * 1000L) + { + continue; + } + + var who = AxSessionMapper.Map(head, database.Version); + findings.Add(CreateFinding(longest >= 300_000 || victims.Count >= 10 ? Severity.High : Severity.Medium, + who.Known + ? $"AX-brugeren bag session {head.SessionId} blokerer {victims.Count} session(er) i {database.Database}" + : $"En AX-session ({head.SessionId}) blokerer {victims.Count} session(er) i {database.Database}", + "En session fra Dynamics AX holder låse, som andre AX-brugere eller batchjob venter på. I AX går alle brugere gennem AOS'ens login, " + + "så SQL Server-login'et siger ikke, hvem det er. " + who.Explanation, + who.Known + ? "Kontakt brugeren (se AX-bruger under målinger) og find ud af, hvad de er i gang med – typisk en stor bogføring, en rapport eller en " + + "dialog, der er efterladt midt i en opdatering. Kan det ikke vente, så afslut brugerens AX-session under Online brugere." + : "Find brugeren i AX under Online brugere ud fra tidspunktet og værten. Afslut sessionen der, hvis det ikke kan vente.", + [ + new Evidence("Database", database.Database), + new Evidence("AX-bruger", who.AxUser ?? "ukendt", Sensitive: true), + new Evidence("AX-session", who.AxSessionId?.ToString(CultureInfo.InvariantCulture) ?? "–"), + new Evidence("Kilde", who.Source), + new Evidence("SQL-session", $"{head.SessionId} ({head.Status}), {head.OpenTransactionCount} åbne transaktioner"), + new Evidence("Vært / program", $"{head.HostName} / {head.ProgramName}", Sensitive: true), + new Evidence("Blokerede sessioner", victims.Count.ToString(CultureInfo.InvariantCulture)), + new Evidence("Længste ventetid", $"{Format.Number(longest / 1000.0)} s"), + new Evidence("Sætning", head.SqlText is { Length: > 500 } s ? s[..500] + " …" : head.SqlText ?? "–", Sensitive: true), + Version(database.Version), + ], + DynamicsAxFindings.Subject(database.Database, $"blokering: session {head.SessionId.ToString(CultureInfo.InvariantCulture)}"))); + } + + return Result(findings); + } + + private static List Chain(BlockingSnapshot snapshot, int head) + { + var result = new List(); + var queue = new Queue([head]); + var seen = new HashSet { head }; + while (queue.Count > 0) + { + foreach (var s in snapshot.BlockedBy(queue.Dequeue()).Where(s => seen.Add(s.SessionId))) + { + result.Add(s); + queue.Enqueue(s.SessionId); + } + } + + return result; + } +} diff --git a/tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs b/tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs new file mode 100644 index 0000000..3ec1081 --- /dev/null +++ b/tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs @@ -0,0 +1,215 @@ +using System.Text; +using System.Text.Json; +using SQLInsight.Collectors.Concurrency; +using SQLInsight.Collectors.DynamicsAx; +using SQLInsight.Core.DynamicsAx; +using SQLInsight.Core.Instances; + +namespace SQLInsight.Collectors.Tests; + +/// +/// "SQL Scripts/<regel-id>.sql": a reviewable script per rule. The header is written by hand; the SQL below the +/// marker is exactly what SQLInsight sends, generated from the code here, so it can never drift. Run the tests with +/// SQLINSIGHT_UPDATE_SQL_SCRIPTS=1 to rewrite the generated part after changing a query. +/// +public sealed class SqlScriptTests +{ + public const string Begin = "-- >>> SQL, som SQLInsight kører – genereret fra koden (tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs). Ret ikke herunder i hånden."; + public const string End = "-- <<< slut på genereret SQL"; + + private static string DynamicsAxDirectory => Path.Combine(AppContext.BaseDirectory, "DynamicsAx"); + + public static TheoryData Files() => [.. Generated().Keys]; + + [Theory] + [MemberData(nameof(Files))] + public void Script_holds_exactly_the_sql_sqlinsight_runs(string file) + { + var path = Path.Combine(RepositoryRoot(), "SQL Scripts", file); + Assert.True(File.Exists(path), $"SQL Scripts/{file} mangler."); + var text = File.ReadAllText(path); + var begin = text.IndexOf(Begin, StringComparison.Ordinal); + var end = text.IndexOf(End, StringComparison.Ordinal); + Assert.True(begin > 0 && end > begin, $"SQL Scripts/{file}: markeringerne for den genererede SQL mangler."); + + var expected = Generated()[file]; + var actual = text[(begin + Begin.Length)..end].Replace("\r\n", "\n", StringComparison.Ordinal).Trim('\n'); + if (actual != expected.Replace("\r\n", "\n", StringComparison.Ordinal).Trim('\n') && Environment.GetEnvironmentVariable("SQLINSIGHT_UPDATE_SQL_SCRIPTS") == "1") + { + var updated = text[..(begin + Begin.Length)] + "\r\n" + expected.Replace("\r\n", "\n", StringComparison.Ordinal).Trim('\n').Replace("\n", "\r\n", StringComparison.Ordinal) + + "\r\n" + text[end..]; + File.WriteAllText(path, updated, new UTF8Encoding(false)); + return; + } + + Assert.Equal(expected.Replace("\r\n", "\n", StringComparison.Ordinal).Trim('\n'), actual); + } + + [Theory] + [MemberData(nameof(Files))] + public void Header_says_what_the_reader_needs(string file) + { + var header = File.ReadAllText(Path.Combine(RepositoryRoot(), "SQL Scripts", file)).Split(Begin)[0]; + + if (!file.StartsWith('_')) + { + Assert.Contains("Regel:", header, StringComparison.Ordinal); + Assert.Contains("Alvorlighed:", header, StringComparison.Ordinal); + } + + Assert.Contains("Rettigheder:", header, StringComparison.Ordinal); + Assert.Contains("Versioner:", header, StringComparison.Ordinal); + Assert.Contains("Collector:", header, StringComparison.Ordinal); + Assert.Contains("ikke verificeret mod kundedata", header, StringComparison.Ordinal); + Assert.DoesNotContain("PXI_", header, StringComparison.OrdinalIgnoreCase); + } + + /// File name → the generated SQL. + internal static Dictionary Generated() + { + var ax30 = Database("schema-ax30"); + var ax40 = AxDetection.Recognise("AXDB", AxSchemaFile.Find(DynamicsAxDirectory, "schema-ax30")!.DetectionRows() + .Select(r => r with { Schema = "dbo", Precision = r.Column == "RECID" ? 19 : r.Precision, Type = r.Column == "RECID" ? "bigint" : r.Type }).ToList(), ["AXDB"])!; + var ax2009 = Database("schema-ax2009"); + var ax2012 = Database("schema-ax2012"); + Assert.Equal((AxVersion.Axapta30, AxVersion.Ax40, AxVersion.Ax2009, AxVersion.Ax2012R2), (ax30.Version, ax40.Version, ax2009.Version, ax2012.Version)); + + var versions = new (string Title, AxDatabase Database)[] + { + ("Axapta 3.0 (skema bmssa)", ax30), + ("AX 4.0 (skema dbo) – ikke verificeret mod kundedata", ax40), + ("AX 2009 – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata", ax2009), + ("AX 2012 RTM/R2/R3 og F&O – baseret på Microsofts dokumenterede skema, ikke verificeret mod kundedata", ax2012), + }; + + string Batch() => Join(versions.SelectMany(v => + { + var adapter = AxSchemaAdapter.For(v.Database.Version)!; + var list = new List { Ax($"{v.Title}: batchjob (AxSchemaAdapter.BatchJobs)", v.Database, adapter.BatchJobs(v.Database)) }; + if (adapter.BatchHistory(v.Database, null) is { } first) + { + list.Add(Ax($"{v.Title}: kørselshistorik, første læsning (AxSchemaAdapter.BatchHistory)", v.Database, first)); + list.Add(Ax($"{v.Title}: kørselshistorik, de næste læsninger fra seneste RECID minus {AxSchemaAdapter.HistoryOverlap}", v.Database, + adapter.BatchHistory(v.Database, Watermark + AxSchemaAdapter.HistoryOverlap)!, watermark: true)); + } + + return list; + })); + + string Numbers() => Join(versions.Select(v => Ax($"{v.Title}: nummerserier (AxSchemaAdapter.NumberSequences)", v.Database, + AxSchemaAdapter.For(v.Database.Version)!.NumberSequences(v.Database)))); + + string RecIds() => Join(versions.Select(v => Ax($"{v.Title}: RecId pr. tabel (AxSchemaAdapter.RecIds)", v.Database, + AxSchemaAdapter.For(v.Database.Version)!.RecIds(v.Database)))); + + var detection = Join( + [ + Plain("Alle versioner: online brugerdatabaser og deres snapshot-indstillinger (AxDatabaseDetector.DatabasesSql)", AxDatabaseDetector.DatabasesSql), + Plain("Alle versioner: agentens login, til GRANT-teksten", "SELECT SUSER_SNAME()"), + Detection(), + Plain("SQL Server 2000 (kun opdagelse under Servere og databaser, AxDetection.MetadataSql2000)", AxDetection.MetadataSql2000("AXDB")), + .. versions.Select(v => Ax($"{v.Title}: firmaer (AxSchemaAdapter.Companies)", v.Database, AxSchemaAdapter.Companies(v.Database))), + ]); + var blocking = Join( + [ + Plain("SQL Server 2012 og nyere (BlockingCollector.BuildSql2005)", BlockingCollector.BuildSql2005(SqlServerVersion.Parse("11.0.7001.0"))), + Plain("SQL Server 2008 og 2008 R2", BlockingCollector.BuildSql2005(SqlServerVersion.Parse("10.50.6000.34"))), + Plain("SQL Server 2005", BlockingCollector.BuildSql2005(SqlServerVersion.Parse("9.0.5000.0"))), + Plain("SQL Server 2000 (BlockingCollector.Sql2000)", BlockingCollector.Sql2000), + ]); + + var files = new Dictionary(StringComparer.Ordinal) + { + ["_collectors/AxDatabaseDetector.sql"] = detection, + ["_collectors/AxBatchCollector.sql"] = Batch(), + ["_collectors/AxSequenceCollector.sql"] = Join([Numbers(), RecIds()]), + ["SQLI-AX-001.sql"] = Batch(), + ["SQLI-AX-002.sql"] = Batch(), + ["SQLI-AX-003.sql"] = Batch(), + ["SQLI-AX-004.sql"] = Batch(), + ["SQLI-AX-005.sql"] = Numbers(), + ["SQLI-AX-006.sql"] = Numbers(), + ["SQLI-AX-007.sql"] = Numbers(), + ["SQLI-AX-008.sql"] = Numbers(), + ["SQLI-AX-009.sql"] = RecIds(), + ["SQLI-AX-010.sql"] = Join([detection, blocking]), + }; + + foreach (var (id, minimum, query) in CommunityAxRules()) + { + files[id + ".sql"] = Join( + [ + Detection(), + $"-- Variant: SQL Server {Release(minimum)} og nyere – i hver overvåget database, som AX-genkendelsen herover har genkendt (CustomQueryCollector)\r\n" + + "USE [AXDB]; -- <== ret til AX-databasen\r\n" + query + "\r\nGO", + ]); + } + + return files; + } + + private const long Watermark = 999_999_999; + + private static string Release(string minimumVersion) => minimumVersion switch { "9.0" => "2005", "10.0" => "2008", "11.0" => "2012", "12.0" => "2014", "13.0" => "2016", _ => minimumVersion }; + + private static AxDatabase Database(string schemaFile) => + AxDetection.Recognise("AXDB", AxSchemaFile.Find(DynamicsAxDirectory, schemaFile)!.DetectionRows(), ["AXDB", "AXDB_model"])!; + + private static string Detection() => Plain("SQL Server 2005 og nyere: AX-genkendelse i hver overvåget database (AxDetection.MetadataSql via AxSql.InDatabase)", + "DECLARE @Database sysname;\r\nSET @Database = N'AXDB'; -- <== ret til databasen\r\nDECLARE @Sql nvarchar(max);\r\nSET @Sql = " + + AxSql.Literal(AxSql.Batch(AxDetection.MetadataSql)) + ";\r\nDECLARE @Procedure nvarchar(300);\r\nSET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql';\r\n" + + "EXEC @Procedure @Sql;\r\n" + AxSql.Reset); + + private static string Plain(string title, string sql) => $"-- Variant: {title}\r\n{sql.Trim()}\r\nGO"; + + /// + /// An AX read as the agent sends it: the batch below runs inside the AX database through [db].sys.sp_executesql + /// (read committed, 2 s lock timeout; snapshot instead when the database allows it), so the session never changes database. + /// + private static string Ax(string title, AxDatabase database, AxQuery? query, bool watermark = false) + { + Assert.NotNull(query); + Assert.True(query.IsMeasurable, query.NotMeasurable); + var sql = new StringBuilder() + .Append("-- Variant: ").Append(title).Append("\r\n") + .Append("DECLARE @Database sysname;\r\nSET @Database = N'AXDB'; -- <== ret til AX-databasen\r\n") + .Append("DECLARE @Schema sysname;\r\nSET @Schema = N'").Append(database.Schema).Append("'; -- <== ejeren af AX-tabellerne: bmssa i Axapta 3.0, ellers dbo\r\n"); + if (watermark) + { + sql.Append("DECLARE @FraRecId bigint;\r\nSET @FraRecId = 0; -- <== SQLInsight bruger den højeste RECID, den har læst, minus ") + .Append(AxSchemaAdapter.HistoryOverlap).Append("\r\n"); + } + + sql.Append("DECLARE @Sql nvarchar(max);\r\nSET @Sql = ").Append(AxSql.Literal(AxSql.Batch(query.Sql!))).Append(";\r\n") + .Append("SET @Sql = REPLACE(@Sql, N'[").Append(database.Schema).Append("].', QUOTENAME(@Schema) + N'.');\r\n"); + if (watermark) + { + sql.Append("SET @Sql = REPLACE(@Sql, N'> ").Append(Watermark).Append("', N'> ' + CAST(@FraRecId AS nvarchar(20)));\r\n"); + } + + return sql.Append("DECLARE @Procedure nvarchar(300);\r\nSET @Procedure = QUOTENAME(@Database) + N'.sys.sp_executesql';\r\n") + .Append("EXEC @Procedure @Sql;\r\n").Append(AxSql.Reset).Append("\r\nGO").ToString(); + } + + private static string Join(IEnumerable parts) => string.Join("\r\n\r\n", parts); + + private static List<(string Id, string Minimum, string Query)> CommunityAxRules() + { + using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(RepositoryRoot(), "rules", "community", "dynamics-ax.json")), + new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + return document.RootElement.EnumerateArray() + .Select(r => (r.GetProperty("id").GetString()!, r.TryGetProperty("minimumVersion", out var v) ? v.GetString()! : "9.0", r.GetProperty("query").GetString()!)) + .ToList(); + } + + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "SQLInsight.slnx"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new InvalidOperationException("Repository-roden blev ikke fundet."); + } +} diff --git a/tests/SQLInsight.Rules.Tests/DynamicsAxRuleTests.cs b/tests/SQLInsight.Rules.Tests/DynamicsAxRuleTests.cs new file mode 100644 index 0000000..d92f21e --- /dev/null +++ b/tests/SQLInsight.Rules.Tests/DynamicsAxRuleTests.cs @@ -0,0 +1,299 @@ +using SQLInsight.Core.Data; +using SQLInsight.Core.DynamicsAx; +using SQLInsight.Core.Findings; +using SQLInsight.Rules.DynamicsAx; +using SQLInsight.Rules.Engine; +using static SQLInsight.Rules.Tests.TestInstances; + +namespace SQLInsight.Rules.Tests; + +/// +/// The Dynamics AX rules on realistic data: AX 3.0 rows as the real customer schema stores them (bmssa, local date + +/// seconds since midnight, 32-bit RecId), interpreted by the AX 3.0 adapter, and AX 2012 rows by the 2012 adapter. +/// +public class DynamicsAxRuleTests +{ + private static readonly DateTime Utc = Now.UtcDateTime; + private static readonly AxThresholds Thresholds = new(); + + private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context)); + + private static AxDatabase Ax30(bool readable = true) => + AxDetection.Recognise("AX30", AxSchemaFile.Find(Path.Combine(AppContext.BaseDirectory, "DynamicsAx"), "schema-ax30")!.DetectionRows(readable), ["AX30"])! + with { AgentLogin = @"KUNDE\svc-sqlinsight$" }; + + /// A BATCH row as AX 3.0 stores it: STARTDATE (local date) + STARTTIME (seconds since midnight), server at UTC+2. + private static AxBatchJobRow Batch30(long recId, int status, DateTime plannedUtc, int classNumber = 4711) => + new(recId, classNumber, null, "dat", "NAT", status, plannedUtc.AddHours(2).Date, (int)plannedUtc.AddHours(2).TimeOfDay.TotalSeconds, + null, null, null, null, -120); + + private static AxBatchSnapshot Batch(params AxBatchJob[] jobs) => new() + { + CapturedAt = Now, Thresholds = Thresholds, Databases = [new AxBatchDatabase { Database = "AX30", Version = AxVersion.Axapta30, Jobs = jobs }], + }; + + private static AxBatchJob Job30(AxBatchJobRow row, AxBatchHistory? history = null, DateTime? firstSeen = null) => + AxSchemaAdapter.Ax30.Interpret(row, history ?? new AxBatchHistory(), "AX30", firstSeen); + + private static AxBatchHistory History(string jobKey, int runs, TimeSpan duration, string database = "AX30") + { + var history = new AxBatchHistory(); + for (var i = 1; i <= runs; i++) + { + history.Add(database, new AxBatchRun(jobKey, "R" + i, "Lagerafslutning", "dat", Utc.AddDays(-i), Utc.AddDays(-i) + duration, AxBatchStatus.Finished)); + } + + return history; + } + + [Fact] + public void Without_ax_databases_the_ax_rules_are_not_relevant() + { + var empty = new AxBatchSnapshot { CapturedAt = Now, Thresholds = Thresholds, Databases = [] }; + var sequences = new AxSequenceSnapshot { CapturedAt = Now, Thresholds = Thresholds, Databases = [] }; + var ax = new AxDatabaseSnapshot { CapturedAt = Now, Databases = [] }; + var rules = BuiltInRules.Create().OfType().ToList(); + + Assert.All(rules, rule => + Assert.Equal(RuleOutcome.NotApplicable, Run(rule, With(empty, sequences, ax, new BlockingSnapshot { CapturedAt = Now, Sessions = [] })).Outcome)); + Assert.Equal(10, rules.Count); + Assert.All(rules, r => Assert.StartsWith("SQLI-AX-", r.Id, StringComparison.Ordinal)); + Assert.All(rules, r => Assert.True(Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.Id))); + } + + [Fact] + public void Without_the_read_grant_the_batch_rules_are_not_measurable_with_the_exact_grant() + { + var reason = AxSchemaAdapter.Ax30.BatchJobs(Ax30(readable: false)).NotMeasurable!; + var snapshot = new AxBatchSnapshot + { + CapturedAt = Now, Thresholds = Thresholds, Databases = [new AxBatchDatabase { Database = "AX30", Version = AxVersion.Axapta30, NotMeasurable = reason }], + }; + + var result = Run(new AxBatchFailedRule(), With(snapshot)); + + Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome); + Assert.Contains(@"GRANT SELECT ON [bmssa].[BATCH] TO [KUNDE\svc-sqlinsight$];", result.Reason, StringComparison.Ordinal); + } + + [Fact] + public void Ax001_a_batch_job_in_error_is_high_and_an_old_error_is_not_reported() + { + var failed = Job30(Batch30(10, 2, Utc.AddHours(-3))); + var old = Job30(Batch30(11, 2, Utc.AddDays(-3), classNumber: 99)); + + var finding = Assert.Single(Run(new AxBatchFailedRule(), With(Batch(failed, old))).Findings); + + Assert.Equal(Severity.High, finding.Severity); + Assert.Equal("database: AX30 · batch: 4711|dat|NAT", finding.Subject); + Assert.Contains("Fejl (STATUS = 2)", finding.Evidence.Single(e => e.Name == "Status i AX").Value, StringComparison.Ordinal); + Assert.Equal(RuleOutcome.Passed, Run(new AxBatchFailedRule(), With(Batch(Job30(Batch30(12, 0, Utc.AddMinutes(10)))))).Outcome); + } + + [Fact] + public void Ax001_a_recurring_job_whose_last_run_failed_is_reported_while_it_waits_for_the_next_run() + { + var row = new AxBatchJobRow(5_000_000_001, null, "Integration til webshop", "dk01", null, 1, Utc.AddHours(1), null, null, null, null, null, 0); + var history = new AxBatchHistory(); + history.Add("AX2012", new AxBatchRun("5000000001", "H1", "Integration til webshop", "dk01", Utc.AddHours(-2), Utc.AddHours(-1), AxBatchStatus.Error)); + var job = AxSchemaAdapter.Ax2012.Interpret(row, history, "AX2012", null); + var snapshot = new AxBatchSnapshot + { + CapturedAt = Now, Thresholds = Thresholds, Databases = [new AxBatchDatabase { Database = "AX2012", Version = AxVersion.Ax2012R2, Jobs = [job] }], + }; + + var finding = Assert.Single(Run(new AxBatchFailedRule(), With(snapshot)).Findings); + + Assert.Contains("Integration til webshop", finding.Title, StringComparison.Ordinal); + Assert.Contains("ikke verificeret mod kundedata", finding.Evidence.Single(e => e.Name == "AX-version").Value, StringComparison.Ordinal); + } + + [Theory] + [InlineData(25, null)] + [InlineData(45, Severity.Medium)] + [InlineData(100, Severity.High)] + public void Ax002_a_job_running_much_longer_than_its_own_history(int runningMinutes, Severity? expected) + { + var row = Batch30(20, 1, Utc.AddHours(-5)); + var job = Job30(row, History("4711|dat|NAT", 10, TimeSpan.FromMinutes(20)), firstSeen: Utc.AddMinutes(-runningMinutes)); + + var result = Run(new AxBatchLongRunningRule(), With(Batch(job))); + + Assert.Equal(expected, result.Findings.SingleOrDefault()?.Severity); + + // Too little history to compare with. + var young = Job30(row, History("4711|dat|NAT", 3, TimeSpan.FromMinutes(20)), Utc.AddHours(-3)); + Assert.Equal(RuleOutcome.Passed, Run(new AxBatchLongRunningRule(), With(Batch(young))).Outcome); + } + + [Theory] + [InlineData(20, null)] + [InlineData(45, Severity.Medium)] + [InlineData(60 * 30, Severity.High)] + public void Ax003_a_waiting_job_past_its_planned_start_is_late_and_after_a_day_never_started(int minutesLate, Severity? expected) + { + var job = Job30(Batch30(30, 0, Utc.AddMinutes(-minutesLate))); + + var finding = Run(new AxBatchLateRule(), With(Batch(job))).Findings.SingleOrDefault(); + + Assert.Equal(expected, finding?.Severity); + if (expected == Severity.High) + { + Assert.Contains("aldrig startet", finding!.Title, StringComparison.Ordinal); + Assert.Contains("lokal dato + sekunder efter midnat", finding.Evidence.Single(e => e.Name == "Tid").Value, StringComparison.Ordinal); + } + } + + [Fact] + public void Ax003_held_jobs_are_not_late() + { + var held = Job30(Batch30(31, 4, Utc.AddDays(-2))); + + Assert.Equal(AxBatchStatus.Hold, held.Status); + Assert.Equal(RuleOutcome.Passed, Run(new AxBatchLateRule(), With(Batch(held))).Outcome); + } + + [Fact] + public void Ax004_a_job_executing_for_hours_is_stuck_unless_it_normally_runs_that_long() + { + var row = Batch30(40, 1, Utc.AddHours(-9)); + var stuck = Job30(row, firstSeen: Utc.AddHours(-7)); + var normallyLong = Job30(row, History("4711|dat|NAT", 10, TimeSpan.FromHours(6)), Utc.AddHours(-7)); + + var finding = Assert.Single(Run(new AxBatchStuckRule(), With(Batch(stuck))).Findings); + + Assert.Equal(Severity.High, finding.Severity); + Assert.Contains("Målt fra første gang SQLInsight så jobbet køre", finding.Evidence.Single(e => e.Name == "Bemærk").Value, StringComparison.Ordinal); + Assert.Equal(RuleOutcome.Passed, Run(new AxBatchStuckRule(), With(Batch(normallyLong))).Outcome); + Assert.Equal(RuleOutcome.Passed, Run(new AxBatchStuckRule(), With(Batch(Job30(row, firstSeen: Utc.AddHours(-1))))).Outcome); + } + + private static AxSequenceSnapshot Sequences(IReadOnlyList? numbers = null, IReadOnlyList? recIds = null, + AxVersion version = AxVersion.Axapta30) => new() + { + CapturedAt = Now, Thresholds = Thresholds, + Databases = [new AxSequenceDatabase { Database = "AX30", Version = version, NumberSequences = numbers ?? [], RecIds = recIds ?? [] }], + }; + + /// A NUMBERSEQUENCETABLE row as AX 3.0 stores it (int LOWEST/HIGHEST/NEXTREC per DATAAREAID, local cleanup date + seconds). + private static AxNumberSequence Number30(string code, long next, long highest = 999_999, string format = "######", bool blocked = false, bool continuous = false, + bool? referenced = true, DateTime? lastCleanLocal = null, params AxSample[] samples) => + AxSchemaAdapter.Ax30.Interpret(new AxNumberSequenceRow(1, code, "dat", "Tekst", 1, highest, next, format, blocked, continuous, false, false, referenced, + lastCleanLocal?.Date ?? new DateTime(1900, 1, 1), (int?)lastCleanLocal?.TimeOfDay.TotalSeconds ?? 0, 24, -120), samples, Utc, 30); + + [Theory] + [InlineData(500_000, null)] + [InlineData(850_000, Severity.Medium)] + [InlineData(960_000, Severity.High)] + [InlineData(1_000_000, Severity.Critical)] + public void Ax005_number_sequence_usage(long next, Severity? expected) => + Assert.Equal(expected, Run(new AxNumberSequenceExhaustionRule(), With(Sequences([Number30("Debitor", next)]))).Findings.SingleOrDefault()?.Severity); + + [Fact] + public void Ax005_forecast_from_sqlinsights_own_samples() + { + // 50 % used, but 10.000 numbers a day: 50 days left. + var sequence = Number30("Ordre", 500_001, samples: [new AxSample(Utc.AddDays(-10), 400_001)]); + + var finding = Assert.Single(Run(new AxNumberSequenceExhaustionRule(), With(Sequences([sequence]))).Findings); + + Assert.Equal(Severity.Medium, finding.Severity); + Assert.Contains("løber tør om ca. 50 dage", finding.Title, StringComparison.Ordinal); + Assert.Equal("database: AX30 · nummerserie: dat/Ordre", finding.Subject); + } + + [Fact] + public void Ax006_format_shorter_than_highest() + { + var tooShort = Number30("Faktura", 12_000, highest: 99_999_999, format: "FA-####"); + var fits = Number30("Faktura", 12_000, highest: 99_999, format: "FA-#####"); + + Assert.Equal(Severity.High, Assert.Single(Run(new AxNumberSequenceFormatRule(), With(Sequences([tooShort]))).Findings).Severity); + Assert.Equal(RuleOutcome.Passed, Run(new AxNumberSequenceFormatRule(), With(Sequences([fits]))).Outcome); + } + + [Fact] + public void Ax007_blocked_but_still_referenced() + { + Assert.Single(Run(new AxNumberSequenceBlockedRule(), With(Sequences([Number30("Gammel", 10, blocked: true)]))).Findings); + Assert.Equal(RuleOutcome.Passed, Run(new AxNumberSequenceBlockedRule(), With(Sequences([Number30("Gammel", 10, blocked: true, referenced: false)]))).Outcome); + Assert.Equal(RuleOutcome.Passed, Run(new AxNumberSequenceBlockedRule(), With(Sequences([Number30("Ukendt", 10, blocked: true, referenced: null)]))).Outcome); + } + + [Fact] + public void Ax008_continuous_sequence_never_or_long_ago_cleaned_up() + { + var never = Number30("Kont", 100, continuous: true); + var recent = Number30("Kont", 100, continuous: true, lastCleanLocal: Utc.AddDays(-1).AddHours(2)); + var old = Number30("Kont", 100, continuous: true, lastCleanLocal: Utc.AddDays(-30).AddHours(2)); + + Assert.Contains("aldrig ryddet op", Assert.Single(Run(new AxNumberSequenceContinuousRule(), With(Sequences([never]))).Findings).Title, StringComparison.Ordinal); + Assert.Equal(RuleOutcome.Passed, Run(new AxNumberSequenceContinuousRule(), With(Sequences([recent]))).Outcome); + Assert.Equal(Severity.Low, Assert.Single(Run(new AxNumberSequenceContinuousRule(), With(Sequences([old]))).Findings).Severity); + } + + [Theory] + [InlineData(1_000_000_000, null)] + [InlineData(1_200_000_000, Severity.High)] + [InlineData(1_700_000_000, Severity.Critical)] + public void Ax009_ax30_recid_is_flagged_early_and_critical_from_75_percent(long next, Severity? expected) + { + var recId = AxSchemaAdapter.Interpret(new AxRecIdRow(-1, "dat", next, 0, null), nextValIsInt: true, [], Utc, 30); + + Assert.Equal(expected, Run(new AxRecIdExhaustionRule(), With(Sequences(recIds: [recId]))).Findings.SingleOrDefault()?.Severity); + } + + [Fact] + public void Ax009_64_bit_recid_only_matters_with_a_lowered_maxval_or_a_fast_forecast() + { + var huge = AxSchemaAdapter.Interpret(new AxRecIdRow(77, null, 5_000_000_000, null, "CUSTTABLE"), nextValIsInt: false, [], Utc, 30); + var lowered = AxSchemaAdapter.Interpret(new AxRecIdRow(77, null, 900, 1_000, "CUSTTABLE"), nextValIsInt: false, [], Utc, 30); + + Assert.Null(AxRecIdExhaustionRule.Assess(huge, Thresholds)); + Assert.Equal(Severity.High, AxRecIdExhaustionRule.Assess(lowered, Thresholds)); + var finding = Assert.Single(Run(new AxRecIdExhaustionRule(), With(Sequences(recIds: [lowered], version: AxVersion.Ax2012R2))).Findings); + Assert.Contains("CUSTTABLE", finding.Title, StringComparison.Ordinal); + } + + private static AxDatabaseSnapshot AxDatabases(AxVersion version) => new() + { + CapturedAt = Now, Databases = [new AxDatabase { Database = "AX", Schema = version == AxVersion.Axapta30 ? "bmssa" : "dbo", Version = version }], + }; + + private static BlockingSnapshot Blocking(string? contextInfo, long waitMs = 120_000) => new() + { + CapturedAt = Now, + Sessions = + [ + new SessionInfo + { + SessionId = 61, Status = "sleeping", DatabaseName = "AX", LoginName = "AXSERVICE", HostName = "AOS01", ProgramName = "Microsoft Dynamics AX", + OpenTransactionCount = 1, ContextInfo = contextInfo, SqlText = "UPDATE INVENTSUM SET …", + }, + new SessionInfo { SessionId = 62, BlockingSessionId = 61, DatabaseName = "AX", WaitTimeMs = waitMs, WaitType = "LCK_M_U" }, + ], + }; + + [Fact] + public void Ax010_ax2012_names_the_ax_user_from_context_info_as_sensitive_evidence() + { + var context = Convert.ToHexString(System.Text.Encoding.ASCII.GetBytes("anne 17")); + + var finding = Assert.Single(Run(new AxBlockingUserRule(), With(AxDatabases(AxVersion.Ax2012R2), Blocking(context))).Findings); + + var user = finding.Evidence.Single(e => e.Name == "AX-bruger"); + Assert.Equal(("anne", true), (user.Value, user.Sensitive)); + Assert.DoesNotContain("anne", finding.Title + finding.Subject, StringComparison.Ordinal); + Assert.Equal("database: AX · blokering: session 61", finding.Subject); + } + + [Fact] + public void Ax010_ax30_says_honestly_that_the_ax_user_cannot_be_seen() + { + var finding = Assert.Single(Run(new AxBlockingUserRule(), With(AxDatabases(AxVersion.Axapta30), Blocking(null))).Findings); + + Assert.Equal("ukendt", finding.Evidence.Single(e => e.Name == "AX-bruger").Value); + Assert.Contains("sætter ikke CONTEXT_INFO", finding.Explanation, StringComparison.Ordinal); + Assert.Equal(RuleOutcome.Passed, Run(new AxBlockingUserRule(), With(AxDatabases(AxVersion.Axapta30), Blocking(null, waitMs: 5_000))).Outcome); + } +} diff --git a/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs b/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs index f07b05b..a293846 100644 --- a/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs +++ b/tests/SQLInsight.Rules.Tests/RuleCatalogTests.cs @@ -18,6 +18,6 @@ public class RuleCatalogTests [Fact] public void Rule_count() { - Assert.Equal(82, BuiltInRules.Create().Count); + Assert.Equal(92, BuiltInRules.Create().Count); } } diff --git a/tests/SQLInsight.Rules.Tests/SQLInsight.Rules.Tests.csproj b/tests/SQLInsight.Rules.Tests/SQLInsight.Rules.Tests.csproj index 7a4f433..bb443d1 100644 --- a/tests/SQLInsight.Rules.Tests/SQLInsight.Rules.Tests.csproj +++ b/tests/SQLInsight.Rules.Tests/SQLInsight.Rules.Tests.csproj @@ -8,6 +8,10 @@ + + + +