-
Notifications
You must be signed in to change notification settings - Fork 1.4k
LogMessageTemplateFormatter - Also use IValueFormatter for positional templates #5955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThe changes update message template formatting logic and its configuration resolution in NLog. They refine how value formatting and quoting is handled, especially for string and DateTime values, and provide a mechanism to override value formatting via dependency injection. Unit tests are extended to verify custom DateTime formatting behavior. Changes
Sequence Diagram(s)sequenceDiagram
participant Test as Unit Test
participant LogFactory
participant OverrideValueFormatter
participant LogMessageTemplateFormatter
Test->>LogFactory: Create instance
Test->>LogFactory: Register OverrideValueFormatter
Test->>LogMessageTemplateFormatter: Create with LogFactory
Test->>LogMessageTemplateFormatter: Format message with DateTime arg
LogMessageTemplateFormatter->>OverrideValueFormatter: FormatValue(DateTime)
OverrideValueFormatter-->>LogMessageTemplateFormatter: Formatted string (custom format)
LogMessageTemplateFormatter-->>Test: Formatted message
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~18 minutes Poem
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (5)
src/NLog/MessageTemplates/ValueFormatter.cs (1)
121-126: Stringify quoting semantics changed; confirm desired behavior for positional vs named holesUsing includeQuotes = (format is null || _legacyStringQuotes) means:
- Named holes with Stringify (format == null) will be quoted by default.
- Positional holes now pass empty string (not null) from LogMessageTemplateFormatter, so they won’t be quoted unless legacy flag is on.
If you want ":l" to also suppress quotes for Stringify, consider this tweak.
- bool includeQuotes = format is null || _legacyStringQuotes; + bool includeQuotes = (format is null || _legacyStringQuotes) + && !string.Equals(format, LiteralFormatSymbol, StringComparison.Ordinal);Please verify behavior for:
- "{0}" with a string (no quotes), "{$0}" with a string (quotes), and "{$0:l}" (no quotes).
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
282-289: Positional holes now flow through IValueFormatter (Serialize/Stringify); subtle quoting shiftPros:
- Unifies formatting pipeline and enables IValueFormatter overrides (as tested).
- Consistent handling across positional/named holes.
Gotchas:
- Normal capture for positional holes now uses Stringify, which may quote strings if legacy flag is on (differs from old string.Format semantics). This seems intentional; please confirm legacy scenarios remain acceptable.
Consider adding a comment here explaining the empty format for Stringify is intentional to avoid quotes for positional placeholders.
tests/NLog.UnitTests/MessageTemplates/RendererTests.cs (3)
109-124: Great targeted test for overriding IValueFormatter (DateTime default format)Solid validation of the new pipeline. Consider also testing:
- Named hole "{time}" (non-positional) to confirm override applies there too.
- "{$0}" and "{$time}" to check Stringify + override interplay and quoting.
I can add these test cases if you want.
148-154: RenderAndTest overload injection is cleanOptional: use a shared LogFactory across related tests to reduce overhead if many future tests will inject services.
156-178: OverrideValueFormatter implementation is correct and minimalMinor nits (optional):
- Accept nullable parameters to mirror IValueFormatter nullability.
- Consider restricting the override to CaptureType.Stringify/Normal if Serialize should bypass (policy-dependent).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/NLog/Config/LoggingConfigurationParser.cs(1 hunks)src/NLog/Internal/LogMessageTemplateFormatter.cs(1 hunks)src/NLog/MessageTemplates/ValueFormatter.cs(3 hunks)tests/NLog.UnitTests/MessageTemplates/RendererTests.cs(3 hunks)
🔇 Additional comments (4)
src/NLog/MessageTemplates/ValueFormatter.cs (3)
171-201: Reordering to prefer IConvertible over IFormattable looks good; watch edge-casesBenefits:
- Avoids treating strings as IEnumerable.
- Keeps format support via SerializeConvertibleObject when provided.
Edge-cases to validate (culture + format interactions):
- DateTime/DateTimeOffset with and without explicit format.
- Decimal/Double with custom numeric formats and cultures.
262-270: AppendFormat for ISpanFormattable is a solid winGood optimization for .NET Core+ without changing semantics. No concerns.
380-394: FormatToString cleanup (pattern matching + AppendFormat path) LGTMClearer code; preserves formatting semantics and ISpanFormattable support.
src/NLog/Config/LoggingConfigurationParser.cs (1)
131-131: Defaulting parseMessageTemplates from ServiceRepository improves consistencyGood move to resolve a default before parsing config. Please verify precedence is unchanged when config explicitly sets PARSEMESSAGETEMPLATES (should override the resolved default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
299-306: Delegate null handling in RenderHole to ValueFormatter for consistencyValueFormatter.FormatValue already handles
nullaccording toCaptureType(JSONnull, stringify"", default"NULL") and will pick up any custom IValueFormatter. Remove the inlinenullcheck so all cases go through the same logic.• File: src/NLog/Internal/LogMessageTemplateFormatter.cs
Method:RenderHole(StringBuilder sb, CaptureType captureType, string? holeFormat, IFormatProvider? formatProvider, object? value)(lines ~299–306)Suggested diff:
- if (value is null) - { - sb.Append("NULL"); - } - else - { - ValueFormatter.FormatValue(value, holeFormat, captureType, formatProvider, sb); - } + // Delegate null handling and all formatting to ValueFormatter.FormatValue + ValueFormatter.FormatValue(value, holeFormat, captureType, formatProvider, sb);Please also add or update unit tests to verify:
CaptureType.Serializewithnull→null- default capture with
null→"NULL"CaptureType.Stringifywithnull→""- custom IValueFormatter overrides null formatting as expected
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/NLog/Config/LoggingConfigurationParser.cs(1 hunks)src/NLog/Internal/LogMessageTemplateFormatter.cs(1 hunks)src/NLog/MessageTemplates/ValueFormatter.cs(3 hunks)tests/NLog.UnitTests/MessageTemplates/RendererTests.cs(3 hunks)tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs(20 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/NLog/Config/LoggingConfigurationParser.cs
- tests/NLog.UnitTests/MessageTemplates/RendererTests.cs
- src/NLog/MessageTemplates/ValueFormatter.cs
🔇 Additional comments (2)
tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)
263-267: Factory helper useslegacyStringQuotes: false— LGTMMatches the new quoting/null-handling expectations in tests.
50-52: No remainingstring.Emptyusages inFormatValuecallsVerified that all
FormatValuecalls consistently usenullfor the format parameter; no instances ofstring.Emptyremain. LGTM.
| if (hole.CaptureType == CaptureType.Serialize) | ||
| { | ||
| MessageTemplates.ValueFormatter.FormatToString(value, hole.Format, formatProvider, sb); | ||
| RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value); | ||
| } | ||
| else | ||
| { | ||
| RenderHole(sb, hole.CaptureType, hole.Format, formatProvider, value); | ||
| RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Don’t force CaptureType.Stringify for positional holes; preserve the hole’s capture type and format
Forcing Stringify changes behavior (e.g., quotes strings) and diverges from the hole’s intended capture semantics. It also conflicts with the tests’ shift to treat null vs empty format distinctly.
Refactor to delegate directly to RenderHole(hole, ...) and stop coalescing format to empty string:
- private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
- {
- if (hole.CaptureType == CaptureType.Serialize)
- {
- RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value);
- }
- else
- {
- RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value);
- }
- }
+ private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
+ {
+ // Preserve capture semantics and pass through original format (including null)
+ RenderHole(sb, hole, formatProvider, value);
+ }This avoids a silent behavior change for positional templates. If forcing Stringify was intentional, please add/adjust tests to assert quoted string behavior for positional holes under the forced renderer.
🏁 Script executed:
#!/bin/bash
# List all occurrences of RenderHole in the formatter to inspect available overloads
grep -R "RenderHole(" -n src/NLog/Internal/LogMessageTemplateFormatter.csLength of output: 788
#!/bin/bash
# Display the RenderHolePositional method and related overloads for context
sed -n '250,330p' src/NLog/Internal/LogMessageTemplateFormatter.cs#!/bin/bash
# Show the RenderHolePositional method for full context
sed -n '260,300p' src/NLog/Internal/LogMessageTemplateFormatter.csPreserve original CaptureType and Format in RenderHolePositional
In src/NLog/Internal/LogMessageTemplateFormatter.cs, the RenderHolePositional overload currently forces all non-Serialize holes to use CaptureType.Stringify and coalesces a null format to string.Empty, which:
- Overrides any other capture semantics (e.g., if new
CaptureTypevalues are introduced in the future) - Conflates a deliberately blank format (
"") with a missing one (null), against the intent of recent tests
Refactor RenderHolePositional to call the existing RenderHole(StringBuilder, in Hole, IFormatProvider?, object?) overload, preserving both the hole’s CaptureType and its raw Format (including null):
--- a/src/NLog/Internal/LogMessageTemplateFormatter.cs
+++ b/src/NLog/Internal/LogMessageTemplateFormatter.cs
@@ -279,10 +279,7 @@ namespace NLog.Internal
private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
{
- if (hole.CaptureType == CaptureType.Serialize)
- {
- RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value);
- }
- else
- {
- RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value);
- }
+ // Preserve the hole’s original capture semantics and format (including null)
+ RenderHole(sb, hole, formatProvider, value);
}If the forced Stringify behavior was intentional for positional placeholders, please document this in a comment and add tests to explicitly assert quoted output under those conditions.
🤖 Prompt for AI Agents
In src/NLog/Internal/LogMessageTemplateFormatter.cs around lines 282 to 289, the
RenderHolePositional method currently overrides the hole's CaptureType to
Stringify for non-Serialize cases and replaces null Format with string.Empty,
which loses the original CaptureType and conflates null and empty formats. To
fix this, refactor RenderHolePositional to call the existing RenderHole overload
that accepts the Hole struct directly, passing the hole as is to preserve its
original CaptureType and raw Format (including null). If the current forced
Stringify behavior is intentional, add a comment explaining this and include
tests to verify the expected output.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (5)
src/NLog/Internal/LogMessageTemplateFormatter.cs (3)
288-289: Confirm intent of using empty string for hole.Format in stringify pathYou pass hole.Format ?? string.Empty for Stringify, but pass hole.Format as-is for Serialize. If ValueFormatter distinguishes null vs empty, this could diverge from named-hole behavior (where null is forwarded). Please confirm this is intentional to preserve string.Format-like behavior (e.g., no quotes for strings) for positional holes, and consider adding tests to lock this in:
- "{0}" with string/date/time/enum (no format) should match string.Format output.
- "{0:O}" and other formats for DateTime/decimal should honor formatProvider.
297-307: Ensure NULL rendering remains consistent with the rest of the pipelineRenderHole appends "NULL" for nulls. Since positional holes now route through this method, null output is unified. Verify that this casing/text matches expectations across all sinks/formatters. If there’s a central setting or constant for null text in ValueFormatter, consider using it for consistency.
321-325: Typo: holeFormatVaue -> holeFormatValueMinor readability fix.
- string holeFormatVaue = sb.ToString(holeStartPosition, holeWidth); + string holeFormatValue = sb.ToString(holeStartPosition, holeWidth); - sb.Append(holeFormatVaue); + sb.Append(holeFormatValue);tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)
137-140: Strengthen assertions to robustly detect unintended flattening.Current NotEqual checks could pass even if output is a flat list with different spacing. Prefer a regex-based assertion that rejects a plain comma-separated list of numbers.
- Assert.NotEqual("1,2,3,4,5,6,7", builder.ToString()); + Assert.DoesNotMatch(@"^\s*\d+(,\s*\d+)*\s*$", builder.ToString());- Assert.NotEqual("1,2", builder.ToString()); + Assert.DoesNotMatch(@"^\s*\d+(,\s*\d+)*\s*$", builder.ToString());Also applies to: 147-150
86-99: Consider adding a positive test for non-null format strings.Since callers now pass null when no format is intended, add a dedicated test that passes a non-null format (e.g., numeric "D4" or DateTime "yyyy-MM-dd") to validate that explicit formats are still honored.
Example you could add:
[Fact] public void FormatValue_AppliesExplicitFormatString() { var value = 12; var builder = new StringBuilder(); var ok = CreateValueFormatter().FormatValue(value, "D4", CaptureType.Normal, CultureInfo.InvariantCulture, builder); Assert.True(ok); Assert.Equal("0012", builder.ToString()); }Happy to provide a PR-ready patch if helpful.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/NLog/Config/LoggingConfigurationParser.cs(1 hunks)src/NLog/Internal/LogMessageTemplateFormatter.cs(1 hunks)src/NLog/MessageTemplates/ValueFormatter.cs(3 hunks)tests/NLog.UnitTests/MessageTemplates/RendererTests.cs(3 hunks)tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs(20 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/NLog/Config/LoggingConfigurationParser.cs
- tests/NLog.UnitTests/MessageTemplates/RendererTests.cs
- src/NLog/MessageTemplates/ValueFormatter.cs
🔇 Additional comments (3)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
280-290: Aligning positional holes with IValueFormatter looks goodSwitching to RenderHole with CaptureType.Serialize/Stringify centralizes null-handling and unifies formatting for positional holes with named holes. This matches the PR goal and simplifies logic.
tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)
263-267: Relocating helper types and factory method improves readability.Moving CreateValueFormatter and the test helper types to the bottom keeps the test cases front-and-center. No functional change; nested type full names remain stable for assertions (e.g., RecursiveTest).
Also applies to: 268-382, 384-418
50-50: All string.Empty usages inFormatValuecalls have been removed
No occurrences ofstring.Emptyas the format parameter remain—our search through the codebase returned zero matches. The updated invocations correctly usenullper the newValueFormattersemantics. Changes are approved.



When having forced NLog to use its own LogMessageTemplateFormatter, then it should always use the NLog IValueFormatter.