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.