Skip to content

[DMS-1228] Make CMS request logging structured#1063

Merged
bradbanister merged 7 commits into
mainfrom
DMS-1228
Jul 8, 2026
Merged

[DMS-1228] Make CMS request logging structured#1063
bradbanister merged 7 commits into
mainfrom
DMS-1228

Conversation

@jleiva-gap

@jleiva-gap jleiva-gap commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread eng/docker-compose/effective-schema-hash.psm1 Fixed
@jleiva-gap jleiva-gap marked this pull request as ready for review June 30, 2026 01:41
@jleiva-gap jleiva-gap requested a review from a team as a code owner July 1, 2026 01:54

@github-advanced-security github-advanced-security 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.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@jleiva-gap jleiva-gap force-pushed the DMS-1228 branch 4 times, most recently from e0b7b13 to 9bae3b8 Compare July 2, 2026 21:00
Squash-rebase of the DMS-1228 structured request logging work onto
origin/main (currently at DMS-1227's 413 handling).

The original pre-rebase history is preserved on the local branch
DMS-1228-pre-rebase-backup. Rebase conflicts were resolved against
DMS-1227's oversized-request-body 413 handling so the LoggingMiddleware
BadHttpRequestException path continues to short-circuit oversized
requests while participating in the structured logging pipeline.

Structured request logging spans both services:
- CMS RequestLoggingMiddleware emits structured start/completion events
  with a per-request BeginScope.
- DMS Core RequestResponseLoggingMiddleware and the DMS frontend
  LoggingMiddleware emit HttpRequestCompleted / HttpRequestFailed
  contract events with sanitized correlation values.
- Shared EventId definitions live in per-service RequestLoggingEventIds
  classes.
- docs/LOGGING.md documents the request-log contract; the docker-compose
  effective-schema-hash module and its Pester tests are included.
Correctness:
- Restore context.TraceIdentifier as the 500 error-response body traceId;
  the sanitized correlation value is used for log events only.
- Emit a Debug "Request started" breadcrumb in CMS request logging so a hung
  request still produces a log line.
- Emit the 413 oversized-body path as an HttpRequestCompleted contract event
  (StatusCode 413) instead of a bare Warning, so it is visible to the collector
  contract; the expected control-flow exception is not attached (S6667 is
  suppressed narrowly with justification).
- Allow empty log output in the effective-schema-hash module (@() -> null).
- Fix "after"/"in" DurationMs template drift in DMS core middleware.
- Restore request identity (method/path/traceId) in the rethrow message so
  host-level logging retains correlation after the scope is disposed.

Cleanup:
- Collapse the CMS duplicate LogError to one nullable-exception call, replace
  scope-dictionary readbacks with locals computed once, and simplify
  GetFailureStatusCode in both frontends.
- Remove the unused HasLongStructuredProperty helper, the duplicate DMS-app
  appsettings test and its now-unused helpers, and the dead legacy
  string-formatter branch; modernize the test TestLoggers.

Docs:
- Document the 413 completion path and the outputTemplate/JSON-formatter footgun
  in LOGGING.md and the DMS appsettings example.

Known tradeoff: the per-request logging scope is still constructed
unconditionally (not IsEnabled-gated) to preserve downstream scope enrichment.
…rilog appsettings binding

Address review findings on the structured request logging work:

- DMS frontend LoggingMiddleware now writes the same sanitized trace id in
  the 500 error response body that the request log events carry as TraceId,
  so a client-reported trace id can be located in the logs when
  CorrelationIdHeader is configured. This also matches the rest of DMS,
  where core FailureResponse bodies already return the correlation-aware
  value. The unit test that previously codified the divergence now asserts
  the body and log values match.

- Add an end-to-end Serilog binding test to both CMS and DMS frontend unit
  suites. Each test loads the application's real appsettings.json, builds
  LoggerConfiguration().ReadFrom.Configuration(...).Enrich.FromLogContext()
  exactly as WebApplicationBuilderExtensions does, captures console output,
  and asserts the emitted line is newline-delimited JSON carrying the
  request-log contract properties, EventId, and RenderedMessage. The file
  sink path is redirected to the test work directory so creating the logger
  does not write into the repository. The existing JSON-shape tests remain
  as fast-failing complements.

- Fix a vacuous-pass hazard in the JSON assertions added on this branch:
  null-conditional chains like properties?["X"]?.GetValue<T>().Should()
  silently skip the assertion when the property is missing. All such chains
  in the two request-logging test files are now parenthesized so a missing
  property flows into the assertion as null and fails.

- Document the request logging EventId values (1228001/1228002) in
  docs/LOGGING.md as the source of truth shared by CMS and DMS, point both
  RequestLoggingEventIds class comments there, and pin the values with unit
  tests in both applications (DMS was missing its pin). EventId equality
  ignores Name, so the tests assert Id and Name separately.

- Document in docs/LOGGING.md that the file sinks intentionally share the
  JSON formatter: file logs carry the same structured properties but are
  not part of the console collector contract and omit RenderedMessage.

Cross-application sharing of the event-id type, test helpers, and log
construction was reviewed and deliberately not done: CMS and DMS build as
separate solutions with no shared assembly, so the documented contract plus
per-application pinned tests are the drift control.
- DMS 413 completions log the status the client actually received when
  the response already started
- CMS GlobalExceptionHandler no longer logs; HttpRequestFailed from the
  request middleware is the canonical (structured) request error event
- CMS request middleware no longer mutates the logging scope after
  BeginScope; StatusCode/DurationMs travel in event state only
- LOGGING.md examples now include EventId and a DMS example with
  RequestLayer; EventId JSON shape pinned in both contract tests
- effective-schema-hash structured-hash regex made explicitly
  case-insensitive for consistency (behavior unchanged; -match was
  already case-insensitive)
- DMS frontend 500 error body now echoes the raw extracted correlation
  value instead of the log-sanitized form. Sanitizing the body was an API
  contract change unique to frontend-generated 500s: every core
  FailureResponse body already returns the raw traceId.Value. Log events
  still carry the sanitized TraceId; body and logs differ only when a
  client-supplied correlation id contains characters outside the logging
  whitelist, and applying the whitelist to a client-reported id yields
  the logged value. A new unit test pins the raw-body/sanitized-log
  divergence; the round-3 alignment test is renamed to match the settled
  contract.

- docs/LOGGING.md failure-contract clarifications: the Exception field
  carries the exception the logging layer itself observed, so a DMS
  frontend HttpRequestFailed for a downstream 5xx has Exception null and
  correlates to the core-layer event via TraceId + RequestLayer. CMS
  handled 400s (malformed bodies, validation failures) are documented as
  intentional HttpRequestCompleted events with no exception payload,
  replacing the previous Error-level exception-handler logs. Corrected
  the claim that CMS rethrows handled exceptions: only exceptions the
  exception handler does not observe propagate through the request
  logging middleware, which logs and rethrows them. The
  GlobalExceptionHandler comment is updated to match.

- Simplified eng/docker-compose/effective-schema-hash.psm1: removed the
  unreachable string/array recursion and PSObject property lookup. The
  only JSON path is ConvertFrom-Json -AsHashtable on lines starting with
  '{', which always yields dictionaries, so event handling is now inline
  in Get-EffectiveSchemaHashFromLogLine.

- LogSanitizer.ReplaceLineEndings is kept with an explanatory comment:
  it is behaviorally redundant with the whitelist (every character it
  strips is already rejected) but CodeQL models ReplaceLineEndings as a
  log-forging sanitizer and does not recognize the custom whitelist loop.

Verified: DMS frontend unit 254 passed, CMS frontend unit 464 passed,
effective-schema-hash Pester tests 9/9, csharpier formatted.
…ests

The two Serilog JSON contract tests in RequestResponseLoggingMiddlewareTests
used null-conditional chains into .Should()
(properties?["X"]?.GetValue<T>().Should().Be(...)), so a missing Properties
node or property short-circuited past the assertion and the test passed
vacuously, leaving the DMS core request-log collector contract unprotected.

All twelve chains in the two test methods are now parenthesized
((properties?["X"]?.GetValue<T>()).Should().Be(...)) so a missing property
flows into the assertion as null and fails. This applies the same fix commit
827cf22 made to the CMS and DMS frontend request-logging test files, which
had missed this DMS core file.

The remaining occurrences of the unparenthesized pattern in the repository
are in pre-existing test files outside this story's diff and are left as-is.

Verified: Given_RequestResponseLoggingMiddleware fixture 8/8 passed,
csharpier formatted.
…tion naming

Address two review findings on the request logging work:

- docs/LOGGING.md claimed request events carry "Exception": null when no
  exception exists, but Serilog.Formatting.Json.JsonFormatter omits the
  Exception field entirely when the log event has no exception attached.
  Since the document is declared the collector contract, rules written
  against a null-valued field would never match. Removed the
  "Exception": null line from both the CMS and DMS examples and rewrote
  the failure-log prose: the field is present only when the logging layer
  itself observed an exception, and collectors must treat it as optional.
  The omission behavior is pinned with ContainsKey("Exception") == false
  assertions in the CMS and DMS frontend JSON shape and appsettings
  binding tests and the DMS core JSON contract test, so a formatter
  change that starts emitting a null field fails a test instead of
  silently diverging from the documented contract.

- Renamed RequestInfo.UnhandledException (added on this branch) to
  CaughtException: the property holds the exception
  CoreExceptionLoggingMiddleware caught and converted into a 500
  response, and its own XML doc already called it a handled exception.
  Updated the XML doc to describe the capture-for-outer-logger intent.
  CoreExceptionLoggingMiddleware's class summary still said it logs
  requests and responses from before this branch removed its LogError;
  it now describes the actual behavior: convert escaping exceptions to
  error responses (403 for authorization failures, 500 otherwise) and
  capture the 500-path exception for the outer request logging
  middleware without logging it. The class name itself is pre-existing
  and left unchanged.

Verified: DMS core Given_RequestResponseLoggingMiddleware 8/8, DMS
frontend Given_LoggingMiddleware 18/18, CMS RequestLoggingMiddleware
tests 18/18, csharpier formatted.
@bradbanister bradbanister enabled auto-merge July 8, 2026 22:00
@bradbanister bradbanister added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 5a177c2 Jul 8, 2026
52 checks passed
@bradbanister bradbanister deleted the DMS-1228 branch July 8, 2026 22:01
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