Repository navigation
Conversation
…into audit-external-bodies
| | `--tag <tag>` | Test a specific ServiceControl image tag, for example a PR prerelease like `pr-1234` | | ||
| | `--persistence <type>` | Persistence for the error instance: `RavenDb` (default), `SqlServer`, or `PostgreSql` | | ||
| | `--error-ingestion-scale-unit <n>` | Start `n` additional error-ingestion-only ServiceControl instances | | ||
| | `--azureblob-body-storage "<connection string>"` | Store message bodies in Azure Blob Storage instead of the container file system. Applies only to `SqlServer` and `PostgreSql`. The error instance and its scale-out units share one container, and each audit instance gets its own | |
There was a problem hiding this comment.
is this missing a "-" between azure and blob?
|
|
||
| External storage is required. The instance fails to start when `MessageBody/StorageType` is missing, which includes existing SQL Server and PostgreSQL Audit instances that upgrade without adding it. Setup creates the directory, container, or bucket. | ||
|
|
||
| Settings use the `ServiceControl.Audit/` prefix in the application settings file. The validation matches the Error instance, whose settings use the `ServiceControl/` prefix. |
There was a problem hiding this comment.
Need to add to docs PR, if not already
| | `MessageBody/MinCompressionSize` | all, minimum body size in bytes to compress | 4096 | | ||
| | `MessageBody/FileSystem/StoragePath` | `FileSystem`, required | none | | ||
| | `MessageBody/FileSystem/DataSpaceRemainingThreshold` | `FileSystem`, free space percentage below which the custom check fails | 15 | | ||
| | `MessageBody/Azure/ConnectionString` | `AzureBlob`, shared key or SAS | none | |
There was a problem hiding this comment.
So we are only supporting authentication via SAS token? What about Managed Identity or other Entra identities?
| @@ -0,0 +1,79 @@ | |||
| # Message body storage | |||
There was a problem hiding this comment.
Any ADRs worth extracting for the message body design?
| var storage = new RecordingBodyStorage(); | ||
| await configuration.CreateRetention(storage).SweepNow(); | ||
|
|
||
| Assert.That(storage.DeletedPrefixes, Has.Some.Matches<string>(bodyId.StartsWith)); |
There was a problem hiding this comment.
Has.Some.Matches(bodyId.StartsWith) also passes for a day prefix, or "2", which would delete every body still in retention.
| return external is null | ||
| ? MessageBodyView.NoContent() | ||
| : MessageBodyView.FromStream(external.Stream, external.ContentType, external.BodySize, version); |
There was a problem hiding this comment.
When the row says External but storage has nothing, this answers 204 with no log. A LogWarning with the body id and row here would make it traceable.
| await foreach (var blob in container.GetBlobsAsync(BlobTraits.None, BlobStates.None, prefix, cancellationToken: cancellationToken)) | ||
| { | ||
| await container.GetBlobClient(blob.Name).DeleteIfExistsAsync(cancellationToken: cancellationToken); | ||
| } |
There was a problem hiding this comment.
This deletes one blob per round trip, in sequence. An hour of audit traffic with binary bodies can be hundreds of thousands of blobs, so the sweep can take as long as the hour it is clearing, and the rows (and on PostgreSQL the partition drop) wait behind it. S3 already deletes 1,000 per call.
BlobBatchClient.DeleteBlobsAsync (256 per batch) would keep Azure in the same range.
| foreach (var filePath in Directory.EnumerateFiles(StoragePath, $"{prefix}*.body")) | ||
| { | ||
| cancellationToken.ThrowIfCancellationRequested(); | ||
| if (Path.GetFileName(filePath).StartsWith(prefix, StringComparison.Ordinal)) | ||
| { | ||
| TryDelete(filePath); | ||
| } | ||
| } |
There was a problem hiding this comment.
Bodies are one flat directory, and this enumeration reads every file in it to find one hour's worth. With a month of audit bodies that's tens of millions of entries per hourly sweep.
Would per-hour subdirectories ({StoragePath}/{yyyyMMddHH}/{guid}.body) work for audit, so the delete becomes Directory.Delete?
| catch (RequestFailedException ex) when (ex.Status is (int)HttpStatusCode.Conflict or (int)HttpStatusCode.PreconditionFailed) | ||
| { | ||
| // A blob for this immutable body already exists. | ||
| } |
There was a problem hiding this comment.
This will swallow every 409 and 412 as "already exists". 409 is also ContainerBeingDeleted and ContainerDisabled, so writes during a container recreate succeed silently and audit rows point at bodies that were never stored.
Filtering on ex.ErrorCode is BlobErrorCode.BlobAlreadyExists or BlobErrorCode.ConditionNotMet would keep the intended case.
|
So this would be a new feature? |
|
So |
Problem
Audit's SQL Server and PostgreSQL persisters discard oversized and binary message bodies. Oversized text retains a searchable prefix, but the body API cannot return the complete body. Error's SQL persisters already support external storage for these bodies.
Change
Reuse Error's FileSystem, Azure Blob, and S3 providers for Audit by extracting them into
ServiceControl.Persistence.BodyStorage.RavenDB behavior is unchanged. Existing
TooLargeandNotTextrows retain their previous API responses; discarded bodies cannot be recovered. The new external-body state uses the existing integer column, so no schema migration is needed.Required configuration
External storage is required for Audit SQL Server and PostgreSQL instances. Existing instances must add
ServiceControl.Audit/MessageBody/StorageTypeand the selected provider's settings before restarting after an upgrade.MaxBodySizeToStorebecomes the inline threshold, not a limit on the complete stored body. Azure defaults to theaudit-bodiescontainer; S3 defaults to theaudit-bodies/key prefix.Each instance must use a separate directory, container, or key prefix. Sharing a location can let one instance's retention process delete another instance's bodies.
Design constraints
Sharing the providers avoids duplicating their storage formats, compression, authentication, and stream handling. Error retains its existing configuration and behavior.
Audit retention deletes by hourly prefixes on SQL Server and daily prefixes on PostgreSQL. This preserves PostgreSQL's partition-drop path without scanning message rows. SQL Server only visits hours containing rows, so bodies orphaned by failed inserts into otherwise empty hours can require manual cleanup.
The current design and configuration are documented in Message body storage.