feat(blob): optional blob event capture to files (Event Grid schema) - #2714
feat(blob): optional blob event capture to files (Event Grid schema)#2714The3G wants to merge 27 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread IBlobEventSink from BlobServer/SqlBlobServer through BlobRequestListenerFactory into all five emitting handlers (AppendBlob, BlockBlob, Blob, PageBlob, Container); add eventSink lifecycle (init/close) to beforeStart/afterClose in both servers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A SAS-authenticated request carries its credential in the query string (e.g. `?...&sig=...`). BlobEventFactory persisted `request.getUrl()` verbatim, which for Express is path + full query, so the SAS signature could be written to a plaintext event file on disk. Strip the query before storing: this removes the credential-leak-to-disk risk and also matches real Azure Storage events, whose `data.url` is the bare blob URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The captured event files are meant to be consumed by an external processor watching the folder. A bare writeFile creates the directory entry before its contents are flushed, so a consumer (or a fast poller) can read an empty/partial *.json file. Write to a <name>.json.tmp first and rename it into place; rename within one filesystem is atomic, so a *.json file is only ever observed complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Poll until the expected event appears instead of a single fixed 200ms sleep, so the tests are reliable under load and on the slower @SQL path. - Name-scope the ContainerCreated filter (was order-dependent) and assert the matching ContainerDeleted event. - Assert data.url is present and query-free (end-to-end complement to the SAS query-strip fix). - Skip not-yet-complete files in the folder reader defensively. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document --blobEventCapture / --blobEventCapturePath in the command line options section and the azurite.blobEventCapture / azurite.blobEventCapturePath VS Code settings, matching the existing house style. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The azurite.blobEventCapture / azurite.blobEventCapturePath settings were declared in package.json, documented, and implemented in VSCEnvironment, but VSCServerManagerBlob never passed them to BlobConfiguration, so the VS Code toggle was a silent no-op. Thread them through, and extract the CLI factory's path-resolution into a shared, unit-tested helper (resolveBlobEventCapturePath) so both entry points behave identically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in “blob event capture” feature that records mutating blob/container operations as Azure Event Grid–schema JSON files on disk, wired through both CLI and VS Code entry points and both LokiJS/SQL backends.
Changes:
- Introduces Event Grid–shaped blob event model + factory and a file-based sink with atomic publish semantics.
- Plumbs an optional event sink through the blob request pipeline and emits events from mutating handlers.
- Adds unit/E2E tests plus CLI/VS Code configuration surface and documentation updates.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/BlobTestServerFactory.ts | Extends test server factory to enable/route blob event capture in tests. |
| tests/blob/resolveBlobEventCapturePath.test.ts | Unit tests for capture-path resolution behavior. |
| tests/blob/FileBlobEventSink.test.ts | Unit tests for file sink atomic writes, filename safety, and self-disable. |
| tests/blob/BlobEventFactory.test.ts | Unit tests for event envelope/schema, SAS query stripping, and sequencer behavior. |
| tests/blob/apis/eventCapture.test.ts | E2E coverage validating events are written when enabled and not written by default. |
| src/common/VSCServerManagerBlob.ts | Wires VS Code settings into blob server configuration for event capture. |
| src/common/VSCEnvironment.ts | Adds VS Code settings accessors for blob event capture flags/path. |
| src/common/Environment.ts | Adds CLI flags for blob event capture and capture path. |
| src/blob/utils/constants.ts | Adds default __blobevents__ folder constant. |
| src/blob/SqlBlobServer.ts | Creates/initializes/closes the optional file event sink in SQL backend. |
| src/blob/SqlBlobConfiguration.ts | Extends SQL config to carry capture enable/path settings. |
| src/blob/IBlobEnvironment.ts | Adds environment interface accessors for capture enable/path. |
| src/blob/handlers/PageBlobHandler.ts | Emits events for page blob create/page writes; plumbs sink into handler base. |
| src/blob/handlers/ContainerHandler.ts | Emits container create/delete events; plumbs sink into handler base. |
| src/blob/handlers/BlockBlobHandler.ts | Emits events for PutBlob/PutBlock/PutBlockList operations. |
| src/blob/handlers/BlobHandler.ts | Emits blob delete events; plumbs sink into handler base. |
| src/blob/handlers/BaseHandler.ts | Adds common emitBlobEvent() helper to safely publish events. |
| src/blob/handlers/AppendBlobHandler.ts | Emits events for append blob create/append writes. |
| src/blob/events/resolveBlobEventCapturePath.ts | Shared logic to resolve effective capture path for CLI + VS Code. |
| src/blob/events/IBlobEventSink.ts | Defines sink interface (init/emit/close) with non-throwing contract. |
| src/blob/events/IBlobEvent.ts | Defines Event Grid–shaped event types and payload structure. |
| src/blob/events/FileBlobEventSink.ts | Implements file sink with sanitization + temp-write + rename publish. |
| src/blob/events/BlobEventFactory.ts | Builds Event Grid–shaped events and strips SAS query strings from URLs. |
| src/blob/BlobServerFactory.ts | Wires CLI flags into blob configuration; resolves default capture path and warns on misconfig. |
| src/blob/BlobServer.ts | Creates/initializes/closes the optional file event sink in LokiJS backend. |
| src/blob/BlobRequestListenerFactory.ts | Plumbs the optional sink into handler construction. |
| src/blob/BlobEnvironment.ts | Adds blob-specific CLI flags accessors for capture enable/path. |
| src/blob/BlobConfiguration.ts | Extends blob config to carry capture enable/path settings. |
| README.md | Documents new CLI flags and VS Code settings and the on-disk schema/behavior. |
| package.json | Adds VS Code extension settings azurite.blobEventCapture and azurite.blobEventCapturePath. |
Suppressed comments (1)
src/blob/BlobRequestListenerFactory.ts:122
ServiceHandleris created without theeventSink, soServiceSubmitBatch(which delegates toBlobBatchHandler) will not emit captured events even when blob event capture is enabled. This breaks the stated goal of capturing mutating blob operations, since batch sub-requests can include create/delete/put operations.
serviceHandler: new ServiceHandler(
this.accountDataStore,
this.oauth,
this.metadataStore,
this.extentStore,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/blob/events/BlobEventFactory.ts:17
sequencerCounteris anumber, so after ~9e15 events it will lose integer precision andnextSequencer()can stop being strictly monotonic. Sincesequenceris intended to reflect ordering, usingbigintavoids precision loss in long-running/high-throughput scenarios.
let sequencerCounter = 0;
function nextSequencer(): string {
sequencerCounter += 1;
return sequencerCounter.toString(16).padStart(64, "0");
}
|
@microsoft-github-policy-service agree |
|
The3G , could you please refresh your PR with main, and address the review comments if any to move this PR forward. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/blob/events/FileBlobEventSink.ts:68
- If writeFile/rename fails, the .tmp file can be left behind in the capture folder, causing unbounded accumulation over time. Consider best-effort cleanup of the temp file on failure.
const tempPath = `${filePath}.tmp`;
const p = writeFile(tempPath, JSON.stringify(event, null, 2))
.then(() => rename(tempPath, filePath))
.catch((err) => {
this.logger.warn(
README.md:462
- README claims this captures “every mutating blob operation”, but the implementation only emits events for a subset (container create/delete; blob delete; block/page/append writes + block list commit). Other mutating operations (e.g. setMetadata/setTags/lease/snapshot/copy) currently don’t emit events, so the wording is misleading. Consider narrowing this sentence (or expanding instrumentation to truly cover all mutations).
Optional. Capture every mutating blob operation as an [Azure Event Grid](https://learn.microsoft.com/azure/storage/blobs/storage-blob-event-overview)-shaped JSON file (one file per event) written into a folder, so the events can be processed later. Disabled by default. Enable it by:
removed the word 'every' as not every event is captured and some events that maybe considered non-mutating like metadata, tags etc. may be confusing to others
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/blob/events/BlobEventFactory.ts:67
eventTimeis generated withnew Date()rather than using the request timestamp already available on the context (context.startTime). This can make captured events (and filenames derived fromeventTime) drift from the actual operation time, and makes tests/consumers harder to correlate with Azurite responseDateheaders (which usecontext.startTime).
return {
topic: `/subscriptions/${DEV_SUBSCRIPTION_ID}/resourceGroups/${DEV_RESOURCE_GROUP}/providers/Microsoft.Storage/storageAccounts/${account}`,
subject,
eventType,
id: randomUUID(),
eventTime: new Date().toISOString(),
dataVersion: "",
metadataVersion: "1",
src/blob/handlers/BaseHandler.ts:35
- The PR description says all mutating blob/container operations are captured, but only a small subset currently emits events (e.g. create/delete container, put/delete blob, put block/list, append/page writes). Other mutating APIs like
ContainerHandler.setMetadataandBlobHandler.setMetadata/setHTTPHeaders/setTags/createSnapshot/startCopyFromURL(and lease operations) do not callemitBlobEvent, so capture is incomplete relative to the stated behavior.
protected emitBlobEvent(
context: Context,
eventType: BlobEventType,
api: string,
props: IBlobEventProps
): void {
README.md:465
- Grammar nit: this sentence refers to a single "operation" and a single "JSON file", but the feature captures multiple operations and produces many files. Pluralizing improves clarity.
Optional. Capture mutating blob operation as an [Azure Event Grid](https://learn.microsoft.com/azure/storage/blobs/storage-blob-event-overview)-shaped JSON file (one file per event) written into a folder, so the events can be processed later. Disabled by default. Enable it by:
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (1)
tests/blob/apis/eventCapture.test.ts:198
- This test currently asserts that
__test_event_capture_off__was not created, but that folder is never configured as the capture path (the server is created with default args). As a result, the assertion will pass even if capture incorrectly writes elsewhere. ConfigureeventFolderas the capture path while keeping capture disabled so the test actually verifies the “path ignored when capture is off” behavior described in the PR.
describe("Blob Event Capture disabled by default @loki @sql", () => {
const factory = new BlobTestServerFactory();
const eventFolder = "__test_event_capture_off__";
const server = factory.createServer(); // no capture args -> disabled
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/common/Environment.ts:114
- The CLI help text has a typo ("telemtry" -> "telemetry"). Since this block was modified in this PR, it’s a good opportunity to fix the user-facing string.
.option(
["", "disableTelemetry"],
"Optional. Disable telemtry collection of Azurite. If not specify this parameter Azurite will collect telemetry data by default."
)
change to string Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/blob/events/BlobEventFactory.ts:17
- The sequencer counter uses a JS number, which will lose integer precision after ~9e15 events (2^53) and can break the monotonic ordering guarantee implied by
sequencer. Using a BigInt counter avoids precision loss while keeping the same 64-hex-char format.
let sequencerCounter = 0;
function nextSequencer(): string {
sequencerCounter += 1;
return sequencerCounter.toString(16).padStart(64, "0");
}
src/common/Environment.ts:113
- Typo in option description: "telemtry" should be "telemetry".
"Optional. Disable telemtry collection of Azurite. If not specify this parameter Azurite will collect telemetry data by default."
src/blob/handlers/BlobHandler.ts:193
- The PR description says blob event capture covers all mutating blob/container operations, but currently only a small subset of mutations emit events (e.g., this DeleteBlob emit plus a few create/write paths). Other clearly mutating BlobHandler operations like setMetadata() (BlobHandler.ts:328+) and setHTTPHeaders() don't emit any event yet, so the feature may be incomplete or the description/docs should be narrowed.
this.emitBlobEvent(context, BlobEventType.BlobDeleted, "DeleteBlob", {});
spelling
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/common/Environment.ts:248
blobEventCapture()treats any provided value as enabled because it checks!== undefined. With theargsparser,--blobEventCapture=false(or similar) can still populate the flag asfalse, which would incorrectly enable capture. Consider treating it as a real boolean value.
public blobEventCapture(): boolean {
if (this.flags.blobEventCapture !== undefined) {
return true;
}
src/blob/BlobEnvironment.ts:165
- Same boolean-parsing issue as
Environment.blobEventCapture(): checking!== undefinedcan incorrectly enable capture when the flag is explicitly set to false (e.g.--blobEventCapture=false).
public blobEventCapture(): boolean {
if (this.flags.blobEventCapture !== undefined) {
return true;
}
// default is false: blob event capture is opt-in
tests/blob/apis/eventCapture.test.ts:199
- This test asserts that
eventFolderis not created, but the server is started withfactory.createServer()(no capture args) and therefore never targetseventFolder. As written, the assertion can pass even if the server accidentally wrote events somewhere else. To make the test meaningful, passeventFolderas the configured capture path while keeping capture disabled so you can assert the path is ignored and the folder is not created.
const factory = new BlobTestServerFactory();
const eventFolder = "__test_event_capture_off__";
const server = factory.createServer(); // no capture args -> disabled
src/blob/events/BlobEventFactory.ts:17
sequencerCounteruses a JSnumber, which loses integer precision beyond 2^53-1. In very long-running or high-volume runs this can break the monotonic guarantee (and could even repeat values). Using abigintavoids precision loss while keeping the same 64-hex-char output.
// Process-level monotonic sequencer. Not per-blob like Azure, but adequate
// for an emulator; vary by index so ordering is observable.
let sequencerCounter = 0;
function nextSequencer(): string {
sequencerCounter += 1;
return sequencerCounter.toString(16).padStart(64, "0");
}
opt-in
opt-in
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/blob/BlobEnvironment.ts:165
blobEventCapture()always returns true when the flag is present, ignoring an explicit false value (for example via a config file or--blobEventCapture=false). This differs fromsrc/common/Environment.tswhich returns the parsed boolean value, and can unintentionally enable capture.
public blobEventCapture(): boolean {
if (this.flags.blobEventCapture !== undefined) {
return this.flags.blobEventCapture;
}
// default is false: blob event capture is opt-in
return false;
}
src/common/Environment.ts:113
- The
--disableTelemetryhelp text still contains a grammar error ("If not specify this parameter"). Since this line was modified, it’s a good opportunity to correct it for CLI UX.
"Optional. Disable telemetry collection of Azurite. If not specify this parameter Azurite will collect telemetry data by default."
src/blob/handlers/ContainerHandler.ts:90
- The PR description says all mutating container operations are captured, but only
create()/delete()emit events here. Other mutating container APIs in this same handler (for examplesetMetadata()at ContainerHandler.ts:205+) do not emit anything, so capture will be incomplete unless those paths are instrumented or the docs/description are narrowed.
this.emitBlobEvent(context, BlobEventType.ContainerCreated, "CreateContainer", {
eTag: etag
});
src/blob/handlers/BlobHandler.ts:193
- The PR description says all mutating blob operations are captured, but this handler currently only emits for
delete(). Other mutating blob APIs in this file (for examplesetMetadata()at BlobHandler.ts:328+,setHTTPHeaders(),setTags(), lease operations, snapshots/copy) do not emit events, so capture will be incomplete unless those paths are instrumented or the docs/description are narrowed.
this.emitBlobEvent(context, BlobEventType.BlobDeleted, "DeleteBlob", {});
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/common/Environment.ts:114
- The updated help text still contains a grammatical error ("If not specify"). Since this string is user-facing CLI output, it should be corrected for clarity.
.option(
["", "disableTelemetry"],
"Optional. Disable telemetry collection of Azurite. If not specify this parameter Azurite will collect telemetry data by default."
)
tests/BlobTestServerFactory.ts:30
- When
enableBlobEventCaptureis set to true but the caller leavesblobEventCapturePathat its default empty string, event capture will silently be disabled because the servers only create a sink whenconfiguration.blobEventCapturePathis truthy. This makes the test factory behave differently from the real entry points (which default to a folder under the workspace location).
oauth?: string,
enableBlobEventCapture: boolean = false,
blobEventCapturePath: string = ""
): BlobServer | SqlBlobServer | LiveModeStubServer {
Summary
Adds an opt-in feature that captures mutating blob operations as Azure Event Grid–schema JSON files on disk, so they can be processed later by external tooling. Controlled entirely by a command-line / configuration switch and off by default.
--blobEventCaptureboolean flag (off by default) plus optional--blobEventCapturePath <dir>(defaults to<location>/__blobevents__); mirrored by VS Code settingsazurite.blobEventCapture/azurite.blobEventCapturePath.${eventTime}-${id}.jsonusing the Event Grid storage event schema (eventType,subject,data.api,data.url,data.eTag,data.contentLength,data.blobType,sequencer, …).BlobServerandSqlBlobServer) and both entry points (CLIBlobServerFactoryand the VS Code extension).Implementation notes
<name>.json.tmpthenrename()), so consumers never observe a partially written file.data.urlhas its query string stripped, so SAS credentials are never persisted to disk.Test plan
npm run test:blob(LokiJS suite): 543 passing, 0 failing, 3 pendingBlobEventFactory(schema + SAS stripping),FileBlobEventSink(atomic write + self-disable),resolveBlobEventCapturePathtests/blob/apis/eventCapture.test.ts: container create/delete, put/delete blob, put block + block list, and disabled-by-default (folder never created)npx eslint src/**/*.ts: cleanNotes
The feature is entirely off unless explicitly enabled, so existing behaviour is unchanged by default. Configuring a capture path without enabling capture is a no-op (a warning is logged and the path is ignored).