Log Template Syntax Valid
ID |
csharp.log_template_syntax_valid |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
code_smell, logging, observability, structured-logging |
Description
Reports a logging call whose message template breaks the placeholder grammar, so the provider cannot parse it. The compiler has nothing to say about it: the template is just a string, and the damage lands on the log entry at run time.
Rationale
A message template is parsed by the logging provider to pair each placeholder with its argument. When that parse fails there is no exception and no build warning — the entry is simply written wrong. The observed outcomes vary by provider: the raw template is emitted with none of the values attached, or the arguments shift by one position so every property afterwards carries a neighbour’s value, or the sink rejects the event and it never appears at all.
The cost is paid at the worst moment. Nobody reads these lines while the system behaves; they are read during an incident, and that is when the missing identifier or the value that belongs to the previous field turns a five-minute diagnosis into a long one. A malformed template also tends to survive for years, because the code path that writes it is usually the error path that rarely runs.
A placeholder is written as an opening brace, an optional destructuring or stringifying prefix, a
name, an optional alignment after a comma, an optional format after a colon, and a closing brace.
Every part of that shape which is actually pinned down is checked: an unmatched brace in either
direction, counting the doubled forms as the escapes they are; a placeholder with nothing inside it,
which names no property and pairs with no argument; a name that begins with a digit without being a
pure argument index, which is neither a valid name nor a valid index; a name carrying anything but
letters, digits and underscore, such as {cache-key} or {user name}; an alignment that is not an
optionally signed whole number, since an alignment is a character count and nothing else; and a
format separator with nothing after it.
The content of a format specifier is the one part left alone. It is handed straight to the provider’s own formatter and is genuinely free-form, so anything non-empty after the colon is accepted.
Distinct from the rule about interpolated log messages: there the string is well formed and the objection is that interpolation builds it eagerly and throws away the structure. Here the string is a template and is malformed.
using Microsoft.Extensions.Logging;
public class Orders
{
private readonly ILogger<Orders> _logger;
public Orders(ILogger<Orders> logger) => _logger = logger;
public void Unclosed(int id)
{
_logger.LogInformation("user {Id did something", id); // FLAW
}
public void Stray(int value)
{
_logger.LogWarning("value } is out of range", value); // FLAW
}
public void Empty(int value)
{
_logger.LogError("rejected value {} outright", value); // FLAW
}
public void Hyphenated(string key)
{
_logger.LogInformation("evicted {cache-key} from the cache", key); // FLAW — not a name
}
public void BadAlignment(long ms)
{
_logger.LogInformation("took {Ms,x} milliseconds", ms); // FLAW — alignment is not a number
}
public void NoFormat(long ms)
{
_logger.LogInformation("took {Ms:} milliseconds", ms); // FLAW — format separator with no format
}
public void WellFormed(int id, string action)
{
_logger.LogInformation("user {Id} did {Action}", id, action); // OK
}
public void Aligned(int id)
{
_logger.LogInformation("aligned {Id,-10:000} value", id); // OK, alignment and format suffix
}
public void FreeFormFormat(int id)
{
_logger.LogInformation("odd {Id: -f0rmat c@n be anything}", id); // OK, format content is free-form
}
}
Remediation
Close the placeholder, name it, or escape the brace that was meant to be literal — a brace that belongs in the output is written twice. When the template is assembled or copied from another format, check it against the placeholder shape: an opening brace, an optional destructuring prefix, a name of letters, digits and underscores that does not start with a digit, an optional alignment after a comma and an optional format after a colon, then a closing brace.
Templates that only run on an error path deserve a test that exercises that path once, since a malformed one will otherwise not be noticed until it matters.