Skip to content

feat(blob): optional blob event capture to files (Event Grid schema) - #2714

Open
The3G wants to merge 27 commits into
Azure:mainfrom
The3G:feature/blob-event-capture
Open

feat(blob): optional blob event capture to files (Event Grid schema)#2714
The3G wants to merge 27 commits into
Azure:mainfrom
The3G:feature/blob-event-capture

Conversation

@The3G

@The3G The3G commented Aug 6, 2026

Copy link
Copy Markdown

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.

  • New --blobEventCapture boolean flag (off by default) plus optional --blobEventCapturePath <dir> (defaults to <location>/__blobevents__); mirrored by VS Code settings azurite.blobEventCapture / azurite.blobEventCapturePath.
  • Captures all mutating blob/container operations (container create/delete, put/delete blob, put block list, page/append writes, …), emitting one JSON file per event named ${eventTime}-${id}.json using the Event Grid storage event schema (eventType, subject, data.api, data.url, data.eTag, data.contentLength, data.blobType, sequencer, …).
  • Writes are fire-and-forget and never fail a storage operation; the sink self-disables if it cannot initialize (mirrors the existing Telemetry precedent).
  • Wired through both backends (LokiJS BlobServer and SqlBlobServer) and both entry points (CLI BlobServerFactory and the VS Code extension).

Implementation notes

  • Event files are published atomically (write <name>.json.tmp then rename()), so consumers never observe a partially written file.
  • The captured data.url has its query string stripped, so SAS credentials are never persisted to disk.
  • Event filename segments are sanitized to prevent path traversal.

Test plan

  • npm run test:blob (LokiJS suite): 543 passing, 0 failing, 3 pending
  • New unit tests: BlobEventFactory (schema + SAS stripping), FileBlobEventSink (atomic write + self-disable), resolveBlobEventCapturePath
  • New end-to-end test tests/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: clean
  • README updated with the new CLI flags and VS Code settings

Notes

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).

The3G and others added 18 commits August 6, 2026 11:35
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>
Copilot AI lite review requested due to automatic review settings August 6, 2026 10:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • ServiceHandler is created without the eventSink, so ServiceSubmitBatch (which delegates to BlobBatchHandler) 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.

Comment thread src/common/VSCServerManagerBlob.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 11:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • sequencerCounter is a number, so after ~9e15 events it will lose integer precision and nextSequencer() can stop being strictly monotonic. Since sequencer is intended to reflect ordering, using bigint avoids precision loss in long-running/high-throughput scenarios.
let sequencerCounter = 0;
function nextSequencer(): string {
  sequencerCounter += 1;
  return sequencerCounter.toString(16).padStart(64, "0");
}

@The3G

The3G commented Aug 6, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@jainakanksha-msft

Copy link
Copy Markdown
Member

The3G , could you please refresh your PR with main, and address the review comments if any to move this PR forward.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

The3G added 2 commits August 13, 2026 15:40
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
Copilot AI review requested due to automatic review settings August 14, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • eventTime is generated with new Date() rather than using the request timestamp already available on the context (context.startTime). This can make captured events (and filenames derived from eventTime) drift from the actual operation time, and makes tests/consumers harder to correlate with Azurite response Date headers (which use context.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.setMetadata and BlobHandler.setMetadata/setHTTPHeaders/setTags/createSnapshot/startCopyFromURL (and lease operations) do not call emitBlobEvent, 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:

Copilot AI review requested due to automatic review settings August 14, 2026 10:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. Configure eventFolder as 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

Copilot AI review requested due to automatic review settings August 14, 2026 10:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."
  )

Comment thread src/blob/events/BlobEventFactory.ts
change to string

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 14, 2026 11:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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", {});

Copilot AI review requested due to automatic review settings August 14, 2026 11:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the args parser, --blobEventCapture=false (or similar) can still populate the flag as false, 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 !== undefined can 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 eventFolder is not created, but the server is started with factory.createServer() (no capture args) and therefore never targets eventFolder. As written, the assertion can pass even if the server accidentally wrote events somewhere else. To make the test meaningful, pass eventFolder as 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

  • sequencerCounter uses a JS number, 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 a bigint avoids 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");
}

Copilot AI review requested due to automatic review settings August 14, 2026 11:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from src/common/Environment.ts which 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 --disableTelemetry help 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 example setMetadata() 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 example setMetadata() 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", {});

Copilot AI review requested due to automatic review settings August 14, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 enableBlobEventCapture is set to true but the caller leaves blobEventCapturePath at its default empty string, event capture will silently be disabled because the servers only create a sink when configuration.blobEventCapturePath is 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 {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants