feat: add UTC timestamps to all console log entries - #3709
Jerry Nixon (JerryNixon) with Copilot wants to merge 17 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to make console logs easier to correlate by prepending an ISO 8601 UTC timestamp (millisecond precision) to console output emitted by both the Service host and the CLI custom logger.
Changes:
- Updated Service logging to use the console “simple” formatter with UTC timestamp settings (including MCP stdio stderr routing for the startup logger factory).
- Updated the CLI custom console logger to prepend a UTC timestamp before the abbreviated log level label.
- Adjusted CLI unit tests for the new timestamp-prefixed output (but the updated assertion is now too permissive).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Service/Program.cs | Switches console logging to include UTC timestamps; MCP stdio path continues to keep stdout clean. |
| src/Cli/CustomLoggerProvider.cs | Prepends UTC timestamps to CLI log prefixes for both standard and MCP stdio modes. |
| src/Cli.Tests/CustomLoggerTests.cs | Updates assertions to account for timestamps in log output. |
| // Apply colors so the abbreviation matches the visual style of engine logs. | ||
| // try/finally guarantees the original colors are restored even if Write throws, | ||
| // otherwise the console would be left tinted (e.g. red on error) for subsequent output. | ||
| string mcpTimestamp = DateTime.UtcNow.ToString(UtcTimestampFormat); |
| } | ||
|
|
||
| TextWriter writer = logLevel >= LogLevel.Error ? Console.Error : Console.Out; | ||
| string timestamp = DateTime.UtcNow.ToString(UtcTimestampFormat); |
| Assert.IsTrue(actual.Contains(expectedPrefix), | ||
| $"Expected output to contain '{expectedPrefix}' but got: '{actual}'"); |
| logging.AddSimpleConsole(options => | ||
| { | ||
| options.TimestampFormat = "yyyy-MM-dd'T'HH:mm:ss.fff'Z' "; | ||
| options.UseUtcTimestamp = true; | ||
| }); |
|
/azp run |
|
Azure Pipelines: Successfully started running 6 pipeline(s). |
|
/azp run |
|
Azure Pipelines: Successfully started running 6 pipeline(s). |
|
Please update the existing hot-reload integration tests to account for this logging migration. These tests replace Console.Out after the host and console logger are constructed. The console provider retains its original writer, so switching the failure diagnostic from Console.WriteLine to the injected ILogger means the tests no longer capture that message and time out. A local probe invoking the actual PR handler confirmed that the warning reaches the original writer, not the test’s replacement writer. The PR’s SQL integration check reports the same four failures:
The corresponding base check passed 1,604 tests with zero failures; this PR passed 1,600 with four failures. Prefer capturing logs through a test ILoggerProvider rather than synchronizing tests on global console output. Alternatively, establish console capture before constructing the host/logger, and restore the original streams in finally. The production migration to ILogger is appropriate; the tests need to be adapted rather than reverting that change. |
|
Please preserve explicitly configured console formats rather than unconditionally assigning ConsoleLoggerOptions.FormatterName = "dab-utc-simple". The normal host path always invokes this registration, overriding an existing Logging:Console:FormatterName setting. A local probe through the actual host-logging configuration confirmed:
This is more than adding timestamps: it changes the output contract for deployments using structured-log collectors or systemd severity extraction. Use the custom formatter for the default/simple path while respecting explicitly selected formats. Handle the UTC timestamp requirement appropriately within each supported format, rather than replacing all formats with plain text. Please add configuration-based regression tests for JSON and systemd selection. The current default-registration tests do not catch this compatibility regression. |
…constructed Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Fixed in 2cecd44. Your diagnosis was right, and investigating it turned up an extra wrinkle worth flagging: the capturing- So I took your second option, which covers both sinks uniformly. The capture writer tees to the original stream rather than swallowing it, so server-initialization and polling diagnostics still show up in CI output — with a plain No database in my sandbox, so I verified the mechanism with two probes instead of the integration tests:
Since the four failures are DB-backed, the SQL integration run is the real confirmation — worth watching that check on this commit. |
Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Fixed in 81e99d5. Reproduced first — with the unconditional assignment, both explicit selections were discarded:
For The stdio factory and Tests in
Mutation-checked both halves: restoring the unconditional assignment fails the json and systemd tests, and removing the One caveat worth calling out. While testing this I found the So those two formats are UTC and millisecond-precise but not calendar-invariant on a non-Gregorian host. Making them invariant would mean reimplementing both formatters (including JSON state/scope serialization), which seemed disproportionate for opt-in formats whose output contract belongs to the collector consuming them — and they were untimestamped entirely before this PR, so this is not a regression. I documented it in the Solution builds with 0 warnings, |
|
The class-level capture change introduces another regression under the actual MSTest lifecycle. MSTest replaces Console.Out between ClassInitialize and each test. The injected console logger retains the writer installed during class initialization, but BootstrapLogger writes through the current Console.Out. Consequently, injected warnings reach the capture while bootstrap success messages bypass it. ClearCapturedLogs() only clears the buffer; it does not reattach the capture writer after MSTest replaces Console.Out. Running the actual hot-reload integration class against an isolated SQL Server LocalDB instance produced 8 failures, 2 passes, and 3 skips. The parsing and validation failure tests now pass, but the eight tests requiring a success message time out. To verify the cause, a temporary harness invoked the same ten original test methods, reattaching the same capture writer before each test and restoring the runner's writer afterward. All ten passed without changing product code or the original test methods. Please preserve the provider-bound writer, reattach that same writer during TestInitialize, and restore the runner's per-test writer during TestCleanup. Do not create a replacement capture writer per test, since the provider would still hold the previous one. Please validate the correction through the actual MSTest integration class, not just a standalone logging probe. |
|
The JSON/systemd format-selection fix works, but the newly supplied timestamps still do not satisfy the invariant ISO-8601 UTC requirement. To distinguish this from a pre-existing issue: the built-in formatters' CurrentCulture behavior already existed. However, with only FormatterName=json or systemd configured, the base revision did not automatically add a timestamp. This PR now supplies the timestamp format, so this is a correctness defect in newly added behavior rather than a regression of previously correct timestamps. With no user-supplied TimestampFormat, the updated PR emits:
The Finnish case also shows that this is not limited to non-Gregorian calendars. Please make DAB-generated timestamps invariant UTC with millisecond precision while preserving JSON/systemd record structure and intentional user-supplied timestamp overrides. Add coverage for these cultures on both formatters. Documenting this behavior in implementation comments does not make the DAB-generated values satisfy the stated timestamp contract. |
|
The formatter-selection fix still breaks supported legacy console settings. A null FormatterName does not necessarily mean that no console format was configured. The older ConsoleLoggerOptions settings still work in .NET 10 and worked in the PR base. Verified differences:
This compatibility regression was already present in the previous PR head, b2dfb45, and remains in 81e99d5. It is not a pre-existing bug on the base branch. Please account for legacy settings when selecting and configuring the replacement formatter, preserving their behavior or mapping them to the equivalent formatter options. Add configuration-based tests for legacy Systemd selection and IncludeScopes. Although these APIs are deprecated, that alone does not justify silently changing settings that the current runtime still supports. |
Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Fixed in 2105d2a, and this time verified against the actual integration class rather than a probe — I got a SQL Server 2022/2025 container running in the sandbox, loaded Your diagnosis was exactly right. Implemented as you specified: Results on the real class:
Mutation check: reducing The one remaining failure is environmental, not a capture problem. So the reload genuinely fails validation in a Linux/SQL-auth sandbox and the test correctly waits for a success message that can never arrive. Notably the failure message was captured fine, which is itself evidence the capture path works — it's the product connection that can't succeed here. I'd expect this to pass on your LocalDB instance; worth confirming on your end since I can't reproduce Windows auth.
|
…gacy ConsoleLoggerOptions support Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com>
Fixed in d336455. You were right that this is a defect in newly added behaviour, not a pre-existing one — the base revision emitted no timestamp at all for those formats, so the culture-dependent value only exists because this PR supplies it. Documenting it wasn't an acceptable resolution. Reproduced through the real and after: What changed. All three share
Process-wide culture is untouched, and the single-provider/MCP-stdout guarantees are unchanged ( Coverage. Mutation-checked: switching 44/44 in Two notes worth your eye:
Fixed in d336455. Confirmed your reading of After: What changed. Ordering is the one subtle part: the marker is populated by touching Coverage, all configuration-driven as you asked (through
Mutation-checked individually: collapsing the legacy One judgement call to flag: legacy |
Why make this change?
Console log output lacks timestamps, making it difficult to correlate events or determine when entries occurred — especially under high request volume.
What is this change?
Prepends an ISO 8601 UTC timestamp with millisecond precision to every console log entry:
src/Service/Program.cs— ReplacesAddConsole()withAddSimpleConsole(TimestampFormat, UseUtcTimestamp)in bothGetLoggerFactoryForLogLevel(startup logger) andCreateHostBuilder.ConfigureLogging(web host logger). MCP stdio path additionally usesServices.Configure<ConsoleLoggerOptions>for stderr routing, keeping a single registered provider.src/Cli/CustomLoggerProvider.cs— PrependsDateTime.UtcNow.ToString(UtcTimestampFormat)before the abbreviated level label in the CLI's custom console logger (both standard and MCP stdio paths). Timestamp format extracted to a named constant.src/Cli.Tests/CustomLoggerTests.cs— UpdatesLogOutput_UsesAbbreviatedLogLevelLabelsassertion fromStartsWithtoContainssince the timestamp now precedes the level label.How was this tested?
Sample Request(s)
No REST/GraphQL/CLI request changes — output-only behavioral change visible when running
dab start.