# ADR: What the store is actually doing

Status: accepted, 2026-09-08. The document store's counterpart of the SQL card
on the Admin tab: every operation this container sent to Azure Cosmos DB, with
the container, the kind, the partition it was pinned to or the fact that it
fanned out, and the request charge beside the milliseconds. Parent: ADR: A
second store on Cosmos DB, and what it costs. The record this one sits beside,
ADR: What the database is actually doing, has an addendum pointing here.

## In plain words

This page adds a live log of every operation the site sends to its document database (Azure Cosmos DB), shown on the Admin tab with what each one cost in request units beside its milliseconds. Like the SQL log beside it, it never shows a value such as an email address.

What that is worth: a developer can see which request caused each operation and whether it touched one partition or all of them, and the organization can show anyone its database costs on a public page with no login.

## Context

The Admin tab's most valuable card for a data-access reviewer is the one that
shows the raw SQL and what each statement cost in time, with nowhere to put a
parameter value (ADR: What the database is actually doing). Cosmos DB has no
SQL log to intercept, and the thing worth showing about it is different: a
statement's cost on Cosmos DB is a number the service returns with every
response, in request units, and whether an operation touched one partition or
every partition is the difference the interview question is about.

## Decision

**A parallel port, not a wider one.** `ISqlLog` and `SqlStatement` are left as
they are. A second port, `IStoreLog`, records a `StoreOperation`:

```csharp
// The document store's counterpart to the SQL log (ADR: What the store is
// actually doing). A parallel port rather than a widening of ISqlLog, because
// the two record different things: a SQL statement has text, parameters and a
// duration; a Cosmos DB operation has a container, a kind, a partition, a
// request charge and a duration, and only sometimes any text at all. One type
// for both would carry nulls on every row, on both sides.
//
// The no-values rule carries over exactly. A parameter here is a
// SqlParameterShape, which has no field for a value, and a partition is a
// description of the partition rather than the key value that named it: the
// key of an account's partition is the account's id, and the key of a claim
// document is an email address.

/// <summary>
/// One operation this application sent to the document store: where, what
/// kind, what it cost in request units and in milliseconds, whether it was
/// pinned to one partition or fanned out, and the HTTP request that caused it.
/// </summary>
/// <param name="At">When the operation ran.</param>
/// <param name="Container">The container the operation went to.</param>
/// <param name="Kind">The kind of operation, one of <see cref="StoreOperationKind"/>.</param>
/// <param name="Text">The query text, or empty when the operation has none.</param>
/// <param name="Parameters">The shape of each query parameter: name, type and size, never the value.</param>
/// <param name="Partition">A description of the partition the operation was pinned to, or that it fanned out; never the key value.</param>
/// <param name="PhysicalPartitions">How many physical partitions the container has for the operation to reach.</param>
/// <param name="RequestCharge">What the operation cost, in request units.</param>
/// <param name="DurationMs">How long the operation took, in milliseconds.</param>
/// <param name="Outcome">The status code, or the failure the operation met.</param>
/// <param name="Request">The HTTP request that caused the operation, like "GET /api/vehicles", or null when none did.</param>
/// <param name="RequestId">The trace identifier of that HTTP request, or null when there was none.</param>
public sealed record StoreOperation(
    DateTimeOffset At,
    string Container,
    string Kind,
    string Text,
    IReadOnlyList<SqlParameterShape> Parameters,
    string Partition,
    int PhysicalPartitions,
    double RequestCharge,
    long DurationMs,
    string Outcome,
    string? Request,
    string? RequestId);

/// <summary>The kinds of operation the store log distinguishes. Strings, because they are read by a page.</summary>
public static class StoreOperationKind
{
    public const string PointRead = "point read";
    public const string PointWrite = "point write";
    public const string PointDelete = "point delete";
    public const string Query = "query";
    public const string Batch = "batch";
    public const string Metadata = "metadata";
}

/// <summary>Port: where recent document store operations are kept for the Admin tab.</summary>
public interface IStoreLog
{
    void Record(StoreOperation operation);
}

/// <summary>The port wired to nothing, for the tests and the relational containers.</summary>
public sealed class NullStoreLog : IStoreLog
{
    public static readonly NullStoreLog Instance = new();

    private NullStoreLog() { }

    public void Record(StoreOperation operation) { }
}
```
*Live from [`api/TheYard.Application/StoreLog.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/StoreLog.cs#L4-L73), region `store-log-port`, read from this build at 1d6b50b.*

The two record different things. A SQL statement has its text and parameters
plus a duration. A store operation has a container, a kind, a partition, a request
charge and a duration, and only sometimes any text. One type for both would
carry nulls on every row on both sides, and the Admin card would be reading a
type to find out which half of it to believe.

**The adapter writes the log, not an interceptor.** There is no command to
intercept: the SDK is called directly (ADR: A second store on Cosmos DB, and
what it costs), and every call goes through one wrapper that times it and reads
the charge off the response. It records the operation whether the call succeeded
or failed:

```csharp
/// <summary>A point read that answers null on 404 rather than throwing, because a missing document is an ordinary answer.</summary>
public async Task<T?> ReadAsync<T>(Container container, string id, string partitionKey, string partitionLabel) where T : class =>
    (await ReadMeasuredAsync<T>(container, id, partitionKey, partitionLabel)).Item;

/// <summary>The same point read, with what it cost handed back to the caller as well as to the log. The experiment card reads through this.</summary>
public async Task<MeasuredItem<T>> ReadMeasuredAsync<T>(Container container, string id, string partitionKey, string partitionLabel) where T : class
{
    var clock = Stopwatch.StartNew();
    try
    {
        var response = await Timed(container, StoreOperationKind.PointRead, "ReadItem", [new SqlParameterShape("id", "String", id.Length)], partitionLabel, 1,
            () => container.ReadItemAsync<T>(id, new PartitionKey(partitionKey)), r => r.Cost());
        return new MeasuredItem<T>(response.Resource, Math.Round(response.RequestCharge, 2), clock.ElapsedMilliseconds);
    }
    catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
    {
        return new MeasuredItem<T>(null, Math.Round(ex.RequestCharge, 2), clock.ElapsedMilliseconds);
    }
}

/// <summary>
/// A point create in one partition. A second create of an id that is
/// already there is refused with 409, which is what the email claim relies
/// on. <paramref name="what"/> is the text the store log shows for it.
/// </summary>
public Task<ItemResponse<T>> CreateAsync<T>(Container container, T document, string partitionKey, string partitionLabel, string what = "CreateItem") =>
    Timed(container, StoreOperationKind.PointWrite, what, [], partitionLabel, 1,
        () => container.CreateItemAsync(document, new PartitionKey(partitionKey)), r => r.Cost());

/// <summary>A replace that carries the etag it read, so a stale write is refused with 412 rather than winning.</summary>
public Task<ItemResponse<T>> ReplaceAsync<T>(Container container, T document, string id, string partitionKey, string? etag, string partitionLabel) =>
    Timed(container, StoreOperationKind.PointWrite, "ReplaceItem (If-Match)", [new SqlParameterShape("id", "String", id.Length)], partitionLabel, 1,
        () => container.ReplaceItemAsync(document, id, new PartitionKey(partitionKey), new ItemRequestOptions { IfMatchEtag = etag }), r => r.Cost());

/// <summary>A point delete of one document by id in one partition; a missing document throws the SDK's 404.</summary>
public Task<ItemResponse<T>> DeleteAsync<T>(Container container, string id, string partitionKey, string partitionLabel) =>
    Timed(container, StoreOperationKind.PointDelete, "DeleteItem", [new SqlParameterShape("id", "String", id.Length)], partitionLabel, 1,
        () => container.DeleteItemAsync<T>(id, new PartitionKey(partitionKey)), r => r.Cost());

/// <summary>
/// A query, every page of it, with the charge of every page added up and
/// one line in the store log saying how many pages it took. Pinned to one
/// partition when the caller can name it, and a fan-out across every
/// physical partition when it cannot, which is the difference the Admin tab
/// exists to show (ADR: The partition key).
/// </summary>
public async Task<IReadOnlyList<T>> QueryAsync<T>(Container container, QueryDefinition query, string? partitionKey, string partitionLabel) =>
    (await QueryMeasuredAsync<T>(container, query, partitionKey, partitionLabel)).Items;

/// <summary>The same query, with the summed charge, the page count and the time handed back as well as logged.</summary>
public async Task<MeasuredQuery<T>> QueryMeasuredAsync<T>(Container container, QueryDefinition query, string? partitionKey, string partitionLabel)
{
    var parameters = query.GetQueryParameters()
        .Select(p => new SqlParameterShape(p.Name, p.Value?.GetType().Name ?? "null", p.Value is string s ? s.Length : null))
        .ToList();
    var options = new QueryRequestOptions { MaxItemCount = -1 };
    if (partitionKey is not null)
    {
        options.PartitionKey = new PartitionKey(partitionKey);
    }
    int physical = partitionKey is null ? PhysicalPartitionsOf(NameOf(container)) : 1;

    var results = new List<T>();
    double charge = 0;
    int pages = 0;
    var clock = Stopwatch.StartNew();
    string outcome;
    try
    {
        using var iterator = container.GetItemQueryIterator<T>(query, requestOptions: options);
        while (iterator.HasMoreResults)
        {
            var page = await iterator.ReadNextAsync();
            charge += page.RequestCharge;
            pages++;
            results.AddRange(page);
        }
        outcome = $"{results.Count} document(s) in {pages} page(s)";
    }
    catch (CosmosException ex)
    {
        charge += ex.RequestCharge;
        outcome = "failed: " + ex.StatusCode;
        Record(container, StoreOperationKind.Query, query.QueryText, parameters, partitionLabel, physical, charge, clock.Elapsed, outcome);
        throw;
    }
    Record(container, StoreOperationKind.Query, query.QueryText, parameters, partitionLabel, physical, charge, clock.Elapsed, outcome);
    return new MeasuredQuery<T>(results, Math.Round(charge, 2), pages, clock.ElapsedMilliseconds);
}

/// <summary>One partition's deletes, at most a hundred at a time, as one atomic batch.</summary>
public async Task<double> DeleteBatchAsync(Container container, string partitionKey, IReadOnlyList<string> ids, string partitionLabel)
{
    double charge = 0;
    for (int start = 0; start < ids.Count; start += 100)
    {
        var slice = ids.Skip(start).Take(100).ToList();
        var batch = container.CreateTransactionalBatch(new PartitionKey(partitionKey));
        foreach (string id in slice)
        {
            batch.DeleteItem(id);
        }
        var response = await Timed(container, StoreOperationKind.Batch, $"TransactionalBatch: {slice.Count} DeleteItem", [], partitionLabel, 1,
            async () =>
            {
                var r = await batch.ExecuteAsync();
                if (!r.IsSuccessStatusCode)
                {
                    throw new InvalidOperationException($"the batch was refused with {r.StatusCode}");
                }
                return r;
            }, r => r.Cost());
        charge += response.RequestCharge;
    }
    return charge;
}

/// <summary>How many documents a container holds, by a count query across every partition. The seed asks this; the probe does not, because a count is a scan.</summary>
private async Task<int> CountAsync(Container container)
{
    var counts = await QueryAsync<int>(container, new QueryDefinition("SELECT VALUE COUNT(1) FROM c"), null, "cross-partition");
    return counts.Count == 0 ? 0 : counts[0];
}

/// <summary>
/// Run one operation and write its charge and its duration to the store
/// log, whether it succeeded or not. The store log is a public page: this
/// records the kind, the container, the shape of the parameters and the
/// charge, and never an id that could be an address or a value that could
/// be anything (ADR: What the store is actually doing).
/// </summary>
private async Task<TResponse> Timed<TResponse>(Container container, string kind, string text, IReadOnlyList<SqlParameterShape> parameters, string partitionLabel, int physical, Func<Task<TResponse>> operation, Func<TResponse, Cost> cost)
{
    var clock = Stopwatch.StartNew();
    try
    {
        var response = await operation();
        var paid = cost(response);
        Record(container, kind, text, parameters, partitionLabel, physical, paid.Charge, clock.Elapsed, paid.Status.ToString());
        return response;
    }
    catch (CosmosException ex)
    {
        Record(container, kind, text, parameters, partitionLabel, physical, ex.RequestCharge, clock.Elapsed, "failed: " + (int)ex.StatusCode);
        throw;
    }
    catch (Exception ex)
    {
        // A refused batch throws its own exception rather than the SDK's,
        // and it is recorded here too, so the one operation that failed is
        // never the one operation the page cannot show.
        Record(container, kind, text, parameters, partitionLabel, physical, 0, clock.Elapsed, "failed: " + ex.GetType().Name);
        throw;
    }
}

// #region record
/// <summary>
/// Write one finished operation to the store log and as one console line:
/// the kind, the container, the charge, the time, the partition described,
/// the outcome and the query text, and never a parameter's value.
/// </summary>
private void Record(Container container, string kind, string text, IReadOnlyList<SqlParameterShape> parameters, string partitionLabel, int physical, double charge, TimeSpan elapsed, string outcome)
{
    // Nothing in here may break the operation it observed
    // (ADR: What the database is actually doing).
    try
    {
        string name = NameOf(container);
        double rounded = Math.Round(charge, 2);
        long ms = (long)elapsed.TotalMilliseconds;
        _log.Record(new StoreOperation(
            DateTimeOffset.UtcNow,
            name,
            kind,
            text,
            parameters,
            partitionLabel,
            physical,
            rounded,
            ms,
            outcome,
            CurrentRequest.Describe(),
            CurrentRequest.Identify()));
        // The same operation as one console line, the shape Entity
        // Framework gives a statement: what ran, what it cost, how long,
        // and the query with its parameters by name and never by value.
        Logger.LogInformation(
            "Executed Cosmos DB {Kind} on {Container} ({Charge} RU, {Ms} ms, {Partition}) {Outcome}: {Text}",
            kind, name, rounded, ms, partitionLabel, outcome, text);
    }
    catch
    {
        // Deliberately silent: a failure to record must never become a
        // failure of the operation, the same reason the SQL interceptor gives.
    }
}
// #endregion record
```
*Live from [`api/TheYard.Infrastructure.Cosmos/CosmosStore.Operations.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Infrastructure.Cosmos/CosmosStore.Operations.cs#L17-L214), region `operations`, read from this build at 1d6b50b.*

**The no-values rule carries over exactly.** A parameter is a
`SqlParameterShape`, which has no field for a value. And a partition is
described rather than named: "pinned to the buyer", "pinned to the account",
"cross-partition". The key of an account's partition is the account's id, and
the key of a claim document is an email address, so a log that printed the
partition key value would print the thing the SQL card was designed never to
print. A point read is logged as `ReadItem` with a parameter named `id` of type
`String` and the id's length, which is the same amount of information the SQL
card gives about `@normalizedEmail`.

**Every fan-out says how far it fanned.** A query with no partition key is
recorded as cross-partition with the container's physical partition count
beside it, read once at startup from the SDK's feed ranges. At this size that
count is one, and the card says one, because "cross-partition, 1 physical
partition" is the honest description of a fan-out to a single server and the
number will change when the data does (ADR: The partition key).

**A query is one line with its pages added up.** The SDK answers a query a page
at a time and charges per page. The log records one line per query with the
charge of every page summed and the page count in the outcome, so a two-page
load reads as one operation that cost what it cost, rather than two lines a
reader has to add.

**Self-observation is filtered the same way.** The health check's two point
reads every thirty seconds, and the reads this page itself causes, are dropped
before they reach the ring, for the reason the SQL ring drops its own: left in,
the card would show nothing but the act of reading it.

## What it looks like

On the Cosmos container the SQL card is replaced by this one. On the relational
container this endpoint answers an empty list and the SQL card stays. One
image and one page, and the page shows whichever store it is on:

```tsx
/** An operation: when, how long, what it cost, who caused it, where it went and what it carried. */
const storeColumns = (window_: CardWindow): Column<StoreOperation>[] => [
  { name: 'At', mono: true, short: true, cell: (operation) => stampFor(window_, operation.at) },
  {
    name: 'Took',
    mono: true,
    num: true,
    cell: (operation) => `${formatNumber(operation.duration_ms)} ms`,
  },
  { name: 'Charge', mono: true, num: true, cell: (operation) => `${operation.request_charge} RU` },
  { name: 'Caused by', mono: true, cell: (operation) => operation.request ?? 'startup' },
  { name: 'Container', mono: true, cell: (operation) => operation.container },
  { name: 'Kind', short: true, cell: (operation) => operation.kind },
  {
    name: 'Partition',
    cell: (operation) =>
      operation.partition +
      (operation.partition.startsWith('cross')
        ? ` (${operation.physical_partitions} physical)`
        : ''),
  },
  {
    name: 'Operation',
    cell: (operation) => (
      <>
        <pre className={styles.sql}>{operation.text}</pre>
        <span className={styles.muted}>{operation.outcome}</span>
      </>
    ),
  },
  { name: 'Parameters', mono: true, cell: (operation) => describeParameters(operation.parameters) },
];

/** The document store's counterpart of the SQL card: every operation with its partition and its charge (ADR: What the store is actually doing). */
function StoreTable({
  log,
  rows,
  window: window_,
  picker,
}: {
  log: StoreLog;
  rows: Fetched<StoreOperation[]>;
  window: CardWindow;
  picker: ReactNode;
}) {
  return (
    <article className={`${styles.wide} op-glass`} data-testid="store-card">
      <h2 className={styles.cardTitle}>What the document store ran</h2>
      <About>
        Every operation this container sent to {log.store}, newest first: the container, whether it
        was a point read, a point write, a query or a batch, whether it was pinned to one partition
        or fanned out across every physical partition, and what it cost in request units beside how
        long it took. Parameters are listed by name, type and size and never by value, and a
        partition is described rather than named, because this page is public and the key of an
        account&rsquo;s partition is the account. Operations caused by this page and by the health
        check are left out. The buffer holds the last 200 in this container&rsquo;s memory and
        empties on every deploy.
      </About>
      {picker}
      {rows === null ? (
        <CardState state="loading" />
      ) : rows === 'failed' ? (
        <p className={styles.muted} data-testid="card-failed">
          Could not read the store log on the last try; the next try is in 30 seconds.
        </p>
      ) : rows.length === 0 ? (
        window_ !== 'now' ? null : (
          <p className={styles.muted}>Nothing recorded yet.</p>
        )
      ) : (
        <DataTable
          label="Operations this application sent to the document store"
          rows={rows.slice(0, window_ === 'now' ? 60 : 200)}
          rowKey={(_operation, index) => index}
          columns={storeColumns(window_)}
        />
      )}
    </article>
  );
}
```
*Live from [`src/components/admin/StoreCard/StoreCard.tsx`](https://github.com/SteveStout/TheYard/blob/1d6b50b/src/components/admin/StoreCard/StoreCard.tsx#L16-L95), region `store-card`, read from this build at 1d6b50b.*

The cold start is the first thing in the log: four `ReadContainer` metadata
reads, then the count queries that decide whether to seed, then the two
cross-partition `SELECT * FROM c` loads of the catalogue and the bids, each with
its charge. After that an idle container records nothing, and a visitor's bid
records a point read and a point write pinned to the buyer, about six request
units between them.

Here is that card on 1.0.0.92 with one visitor's whole session in it, newest
first: a reset (a query pinned to the buyer's partition and a transactional
batch of one delete, 7.78 RU together), a raise (a point read and a
`ReplaceItem` with `If-Match`, 11.29 RU), a first bid (a point read that finds
nothing, which costs the same 1 RU as one that does, and a `CreateItem`), a
sign-in (two point reads, 2 RU), and a registration (two creates and the two
reads that looked for a claim first, 13.04 RU):

![What the document store ran: one session's reset, raise, bid, sign-in and registration as operations, each with its partition, its request charge and its time](/api/docs/images/cosmos-cosmos-store.png#960x1096)

The same three minutes on the relational container, from its own card, where
the same reset and raise are a `DELETE` with one parameter and an `UPDATE`
guarded by `RowVersion`, at 44 and 41 ms against the document store's 6 and 6:

![The SQL this application ran: the same session's reset and raise as statements, with their parameters listed by name and type and never by value](/api/docs/images/cosmos-sql-store.png#960x1068)

The two cards are the same page reading the same ring, and the columns differ
because the stores do: the relational card has nothing to say about a
partition or a charge, and the document card has no statement to show, only
the operation and what it cost.

## What the tests hold

The same canary as the SQL card, on both stores: register an address, read what
the store ran, assert the users container is in it and the address is not, in
the operations and in the log lines. `AdminObservabilityTests` asks the
container which store it is on and reads the matching card, so one test holds
the rule against both:

```csharp
/// <summary>
/// Whichever store this container is on, what it ran. The SQL section on a
/// relational container, the operations section on a document one, so the
/// same test holds the no-values rule against both stores rather than
/// against the one it was written for (ADR: What the store is actually
/// doing).
/// </summary>
private async Task<(bool Cosmos, string Body)> WhatTheStoreRan()
{
    string store = await _client.GetStringAsync("/api/admin/store");
    using var json = JsonDocument.Parse(store);
    bool cosmos = json.RootElement.GetProperty("store").GetString() == "Azure Cosmos DB";
    return cosmos
        ? (true, json.RootElement.GetProperty("operations").GetRawText())
        : (false, await _client.GetStringAsync("/api/admin/sql"));
}
```
*Live from [`api/TheYard.Tests/AdminObservabilityTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/AdminObservabilityTests.cs#L20-L35), region `what-the-store-ran`, read from this build at 1d6b50b.*

And against the real account, the store tests assert that every operation
carries a charge, that the only cross-partition operations are the loads, and
that no operation's text or partition carries the address (ADR: Accounts on a
document store).

## Consequences

- A reviewer can watch the operations this container sends to Cosmos DB, see
  which request caused each one, and see what each cost in request units, on a
  public page, with no login and no portal.
- The comparison card has a number to put beside every millisecond on the
  document side, which is what makes it a comparison rather than two stopwatches.
- One more fixed-size ring in memory, two hundred operations, emptied on every
  roll.
- The relational side did not change. `ISqlLog`, the interceptor and the SQL
  card are as they were, and a container on SQL Server never sees the new type.

## Addendum, 2026-09-09: one console line per operation, like every statement

Looking at both Admin tabs after the two-sites lane, Steve noticed the Cosmos DB
side had no log like the SQL side's, and asked for one, even a log of the API
requests. He was
reading the last card, the log as the console got it. On the relational side
that card shows every statement, because Entity Framework logs each command
at Information and the ring captures that one framework category. The
document store had its own card above with every operation, its charge and
its partition, and nothing in the console log at all, so the third place the
page shows a store's traffic showed one store.

From 1.0.0.103 the store writes one line per operation to the application's
logger, in the shape Entity Framework gives a statement: what ran, on which
container, what it cost in request units, how long it took, the partition
described, the outcome and the query with its parameters by name. Never a
value, for the reason this record already gives. The logger is attached by
the host after the container is built, the way the relational side attaches
Entity Framework's, so a startup operation on a developer's machine is
silent and everything after the application exists is on the console, in
Application Insights, and on the card. A test asks the container for a
request served by the document store and reads the line back under the
store's own category, and checks the address it looked up is nowhere on the
page.

```csharp
/// <summary>
/// Write one finished operation to the store log and as one console line:
/// the kind, the container, the charge, the time, the partition described,
/// the outcome and the query text, and never a parameter's value.
/// </summary>
private void Record(Container container, string kind, string text, IReadOnlyList<SqlParameterShape> parameters, string partitionLabel, int physical, double charge, TimeSpan elapsed, string outcome)
{
    // Nothing in here may break the operation it observed
    // (ADR: What the database is actually doing).
    try
    {
        string name = NameOf(container);
        double rounded = Math.Round(charge, 2);
        long ms = (long)elapsed.TotalMilliseconds;
        _log.Record(new StoreOperation(
            DateTimeOffset.UtcNow,
            name,
            kind,
            text,
            parameters,
            partitionLabel,
            physical,
            rounded,
            ms,
            outcome,
            CurrentRequest.Describe(),
            CurrentRequest.Identify()));
        // The same operation as one console line, the shape Entity
        // Framework gives a statement: what ran, what it cost, how long,
        // and the query with its parameters by name and never by value.
        Logger.LogInformation(
            "Executed Cosmos DB {Kind} on {Container} ({Charge} RU, {Ms} ms, {Partition}) {Outcome}: {Text}",
            kind, name, rounded, ms, partitionLabel, outcome, text);
    }
    catch
    {
        // Deliberately silent: a failure to record must never become a
        // failure of the operation, the same reason the SQL interceptor gives.
    }
}
```
*Live from [`api/TheYard.Infrastructure.Cosmos/CosmosStore.Operations.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Infrastructure.Cosmos/CosmosStore.Operations.cs#L174-L213), region `record`, read from this build at 1d6b50b.*

## Where it sits

Three rings share this one: Application owns the `IStoreLog` port and the `StoreOperation` record in `StoreLog.cs`, Infrastructure.Cosmos fills it from the one wrapper in `CosmosStore.Operations.cs` that times every SDK call and reads its charge, and the Api host keeps the ring the Admin tab's `StoreCard` reads from. Interface segregation, small interfaces shaped to what a caller needs, is the plain reason for a parallel port: `ISqlLog` stayed as it was, so a container on SQL Server never meets a type full of request charges and partitions it has no use for. Dependency inversion follows too, since the adapter writes to an interface Application defined and the host decides where the lines end up. The price is one more fixed ring of two hundred operations in memory and a second card to keep in step with the SQL one, and a system with many store types would probably push this into OpenTelemetry spans instead of a hand-built ring per store.

## Files

- [`api/TheYard.Application/StoreLog.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/StoreLog.cs): the type with nowhere to put a value, and the port.
- [`api/TheYard.Infrastructure.Cosmos/CosmosStore.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Infrastructure.Cosmos/CosmosStore.cs): the wrapper every operation goes through.
- [`api/TheYard.Api/AdminObservability.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/AdminObservability.cs): the ring and the window's numbers.
- [`api/TheYard.Api/Endpoints/AdminEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Endpoints/AdminEndpoints.cs): the endpoint (region store-endpoint).
- [`api/TheYard.Api/MetricsReport.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/MetricsReport.cs): the request charge in the metrics.
- [`src/components/admin/StoreCard/StoreCard.tsx`](https://github.com/SteveStout/TheYard/blob/main/src/components/admin/StoreCard/StoreCard.tsx): the card.
- [`api/TheYard.Tests/AdminObservabilityTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/AdminObservabilityTests.cs): the canary, on both stores.
- [`docs/decisions/ADR-043-what-the-database-is-doing.md`](https://github.com/SteveStout/TheYard/blob/main/docs/decisions/ADR-043-what-the-database-is-doing.md): the SQL card this one is the sibling of.
