Table of Contents

Logging

Three different things are called logging here, with different purposes, different durability and different audiences. Confusing them is how people end up looking in the wrong place.

What it is Lives Read by
Application log Framework ILogger output — faults and diagnostics The platform's log stream, transient Engineers
Database logs Append-only business records — email sent, payment calls, queued work The database, permanently Support, and the code itself
Audit fields Created and modified timestamps on entities The row itself Anyone asking "when did this change"

The middle one is the surprise: some of it is load-bearing. The email log is not a record of what happened, it is what stops a redelivered message being sent twice.

The application log

Standard framework logging. No third-party logging library, no custom sink, no enrichment. Levels default to Information, with framework noise turned down to Warning.

It is also deliberately sparse — around thirty log statements across the whole codebase, most of them Error or Warning. The code does not narrate what it is doing.

Level discipline is the point

The interesting decision is not what gets logged but what deliberately does not.

Situation Level Why
A domain exception mapped to a status code Debug A stock-out, a basket conflict, a declined payment are expected outcomes. The caller already got a status code
The client disconnected mid-request Debug A customer switching account reloads the page, cancelling every in-flight request. Normal, and frequent
A mapped exception arriving after the response started Error Genuinely wrong — the response cannot be corrected
An outbound call to an external system failed Error Something needs attention
A provider fell back to an alternative Warning Worth noticing, not worth waking anyone

The reasoning is recorded in the middleware itself, and it is worth quoting because it is the principle the whole approach rests on: a steady drip of benign exceptions is how a real one gets missed.

An error in the log should mean somebody has to do something. Anything that is a normal outcome — however unwelcome to the customer — belongs at Debug, where it can be turned up when you are actually investigating.

This is why an apparently quiet log is not evidence that nothing is happening.

How the middleware logs

Three middlewares touch logging, in pipeline order from the outside in.

Middleware Logs
Public API problem details — outermost Error on any unhandled exception under the public surface, including the correlation id
Domain exception Debug for a mapped exception or a client disconnect. Error only when the response had already started
Correlation id Nothing at all

The public-API handler sits outermost so that a failure anywhere inside cannot leak an internal error shape to an external integrator — and it is the one place in the application where a correlation id reaches a log entry.

The correlation id does less than it looks like it does

The middleware reads X-Correlation-Id from the request, generates one when absent, stores it on the request context, and echoes it on the response. That is all it does.

Warning

It does not create a log scope, so log entries do not carry the correlation id. Exactly one place logs it — the public API's unhandled-exception path. Everywhere else, the id exists on the request and never reaches the log.

Nor is it stamped onto queued work. Events carry their own identifiers; none carries a correlation id. So it does not follow background processing, and an order's ERP post cannot be tied back to the request that created it by correlation id.

The middleware's own comment describes downstream code stamping it onto outbound events and log scopes. That is the intended design, not the current state.

What it is good for today: the response header, so a caller can quote it, and correlating an integrator's own logs with a timestamp. What it is not good for: searching the application log.

Adding a log scope in that middleware would make every entry in a request carry the id automatically. It is a small change and would materially improve diagnosis.

Database logs

Three append-only records, and the differences between them matter.

Email sent log

One row per message accepted by the provider: template, recipient, subject, related entity, timestamp and the provider's message id.

It is load-bearing. The order acknowledgement handler checks it before sending, so a redelivered queue item does not email the customer twice. Its index is deliberately not unique, because the log is an append-only audit trail rather than a uniqueness constraint.

Visible and searchable from the back office. Searching by related entity is how you get from an order number to its acknowledgement.

Payment log

One row per payment provider call: operation, both payloads, status code, provider reference, success and any error.

It is best-effort. If the row cannot be written, the failure is logged to the application log and swallowed — the provider call has already happened, and an audit-write failure must not become a second failure mode in front of the customer. The trade is deliberate: a lost audit row is better than a customer seeing an error for something that actually succeeded.

No back-office screen. Database access only, which is why a payment question is an escalation.

Queued work

Every async item, with its status, retry count, next-attempt time and full payload. Searchable and retryable from the back office.

Audit fields

Every auditable entity carries created and modified timestamps, stamped on save by the shared context base and owned by Commerce, never by the sender. They are not a log, but they answer when a row last changed — often the fastest way to tell whether a push has arrived.

Application Insights

Telemetry goes to Application Insights, but note how it gets there: the connection string is injected by the infrastructure and there is no telemetry SDK in this codebase. Instrumentation is attached by the platform, so what arrives is what the platform agent collects.

Reliably there Do not assume
Requests — path, status, duration Application log traces. Verify in the portal
Dependencies — SQL, outbound HTTP Custom events or metrics. Nothing emits any
Unhandled exceptions with stack traces Business telemetry such as orders placed
Performance counters Anything tagged by customer

Practical consequence: for business questions — did this email send, did this payment go through, did this order reach the ERP — the database logs are the better source, and they have screens. Application Insights is for "is the platform healthy and what is slow".

What is not logged anywhere

Back-office actions Who changed a setting, edited a template or revoked a session. Rows carry timestamps, but no actor and no action history
Card details Never stored, never logged
Correlation on log entries See above
Structured properties With no structured logging library, querying is limited to what a message template happens to include