Add EF Core SQL Server and PostgreSQL persisters to the audit instance - #5936
Merged
Merged
Conversation
johnsimons
added this pull request to stack #5937
September 28, 2026 07:51
johnsimons
force-pushed
the
john/ef_for_audit
branch
2 times, most recently
from
September 28, 2026 22:54
76aaefa to
7707e3c
Compare
rbev
approved these changes
Sep 29, 2026
rbev
left a comment
Contributor
There was a problem hiding this comment.
Looks good, a just a few minor questions
| public const string SchemaKey = "Database/Schema"; | ||
| public const string CommandTimeoutKey = "Database/CommandTimeout"; | ||
| public const string QueryTimeoutInSecondsKey = QueryTimeLimit.SettingName; | ||
| internal const string QueryTimeoutSettingName = "ServiceControl.Audit/" + QueryTimeLimit.SettingName; |
Contributor
There was a problem hiding this comment.
Is this the normal convention for the setting name?
Member
Author
There was a problem hiding this comment.
Yes, for the audit instance, we tend to prefix settings with ServiceControl.Audit/.
johnsimons
force-pushed
the
john/ef_for_audit
branch
from
September 29, 2026 22:55
7707e3c to
0afe682
Compare
Introduces two new persistence backends for ServiceControl Audit using Entity Framework Core: PostgreSQL (with range partitioning on created_on for efficient retention) and SQL Server (with full-text search via SQL Server FTS). Each persister ships with its own DbContext, migrations, partition/retention manager, full-text search dialect, and retention lock implementation. Also adds corresponding persistence and acceptance test projects for both providers, and wires them into the cloud database CI workflow so they run alongside the existing primary instance tests.
…d clarity Moves `MessageRow` and `UpsertExtensions` into their own files with proper namespaces, renames `BodyClassifier` to `MessageBodyClassifier` (and `MayBeText` to `MightBeText`) for clearer naming, relocates an unpooled-connection comment to sit closer to the relevant field, and expands the partition custom check message to guide users on remediation steps.
…sues Moves the UtcDateTimeConverter registration from SqlServerAuditDbContext into the shared AuditDbContext so both SQL Server and PostgreSQL benefit from it. Because EF applies these converters to query parameters as well, manual AsUtc calls in ingestion and query code are no longer needed and are removed. Also fixes the truncation separator from '#' to '~' to avoid URL-encoding issues in conversation IDs, and switches the JSON header encoder to UnsafeRelaxedJsonEscaping so apostrophes, plus signs and non-ASCII letters are stored as-is rather than escaped, enabling full-text search to find words adjacent to them.
johnsimons
force-pushed
the
john/ef_for_audit
branch
from
September 30, 2026 02:40
3b7a5a8 to
238e94f
Compare
| using ServiceControl.Audit.Persistence.EFCore.DbContexts; | ||
| using ServiceControl.Audit.Persistence.EFCore.Infrastructure; | ||
|
|
||
| sealed class AuditRetention( |
Contributor
There was a problem hiding this comment.
Audit needs the same treatment as primary here i.e. ServiceControl.Persistence.EFCore/Infrastructure/RetentionSweepCustomCheck.cs
…stently failing Introduces AuditRetentionCustomCheck, which fails once three or more consecutive retention sweeps have failed, surfacing the issue through the standard ServiceControl health-check mechanism with a link to troubleshooting guidance. Also expands the email notification and internal classification lists to cover additional audit and error custom checks that were previously missing.
rbev
approved these changes
Oct 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Add SQL Server and PostgreSQL persisters to the audit instance, built on EF Core. They implement the audit persistence contracts and pass the shared audit persistence and acceptance tests. The few shared tests that cover a deliberate difference from RavenDB are excluded, as listed below.
The persisters ship dormant, the same way the primary instance's EF persisters did. The installer still creates RavenDB audit instances only, and there are no public docs yet.
Configuration
ServiceControl.Audit/PersistenceTypeSQLServerorPostgreSQLServiceControl.Audit/Database/ConnectionStringServiceControl.Audit/Database/SchemaServiceControl.Audit/Database/CommandTimeoutServiceControl.Audit/QueryTimeoutInSecondsSetup applies the EF migrations. The migrations history is kept in its own table,
__AuditMigrationsHistory, so an audit instance can share a database and schema with a primary instance. Maintenance mode is not supported.Storage and retention
Each audit message is a plain insert, stamped with the hour it was ingested (
created_on). The primary key is(created_on, id), with a database-generatedid.PostgreSQL
Both audit tables are partitioned by day, and retention drops expired partitions whole.
SQL Server
The tables are not partitioned, because a full-text index cannot be aligned to a partition scheme. Retention deletes each expired hour in batches of 4,000 rows, below the 5,000 locks at which SQL Server escalates to a table lock.
Both providers
The sweep runs hourly under a session lock (
pg_try_advisory_lockorsp_getapplock) held on an unpooled connection. Only one host sweeps a schema at a time, and a host that crashes releases the lock when its connection drops.Search and bodies
Full-text search is always on for these persisters, whatever
EnableFullTextSearchOnBodiesis set to.simpletext search configuration, over the headers, the message type and the first 262,144 characters of the body. The cap is there becauseto_tsvectorfails once a document's lexemes pass 1 MB. Search terms are ORed, as on RavenDB.FREETEXTover the headers and the body. The index lives in theServiceControlFullTextCatalogcatalog, shared with the primary instance, and setup fails if Full-Text Search is not installed.JavaScriptEncoder.Create(UnicodeRanges.All). The default encoder would store non-ASCII header values, such as localized exception messages, as escape sequences that search cannot match.Bodies are stored in the message row, and only text is kept:
MaxBodySizeToStoreMaxBodySizeToStoreThe body URL includes the ingestion hour, so a body lookup reads a single hour of the table.
Queries
Total-Countand the paging links report the cap when it is reached. Saga history is capped at 50,000 changes, as on RavenDB.MessageViewComparer, which compares the numeric value, so an instance sorting any other way could leave rows out of the merged page.timestamptzparameter that is not UTC.#, then a SHA-256 hash of the whole value. A value converter applies the same function to lookups, so conversation queries stay exact. Message IDs have no limit, and every transport keeps endpoint names well under it.Differences from RavenDB
The shared tests for the first three are excluded from the EF test projects.
Tests
ServiceControl.Audit.Persistence.Tests.SqlServerand.PostgreSql, andServiceControl.Audit.AcceptanceTests.SqlServerand.PostgreSql. EF-specific tests live inServiceControl.Audit.Persistence.Tests/EFCore/, which the RavenDB project excludes.Local results, Release build: