Skip to content

file source: encoding.charset = "auto" - safe per-file encoding detection (BOM + UTF-8/UTF-16 only) #25815

Description

@klondikedragon

A note for the community

I plan on submitting a PR implementing this enhancement, and I'd love feedback on the design before I submit the PR.

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

Use Cases

Collecting Windows log files. Windows log directories routinely mix encodings:

  • Windows servicing logs (C:\Windows\Logs\CBS\CBS.log,
    C:\Windows\Logs\DISM\dism.log) are UTF-8 on some Windows versions and
    UTF-16 on others: the same file path needs a different fixed charset on
    different systems in the same fleet, so no single static config is correct.
  • SQL Server ERRORLOG is UTF-16LE (version-dependent, sometimes without BOM).
  • MSI/installer logs are UTF-16 or ANSI depending on the producer, and can
    change encoding across rotations of the same logical log.
  • PowerShell redirection produces UTF-16LE with BOM; many app logs next to
    these files are UTF-8/ASCII.

encoding.charset is per-source today, and the line delimiter is encoded into
that charset for framing, so a single file source with a directory glob cannot
handle a mix of encodings no matter how it is configured. Splitting into one
source per encoding requires knowing each file's encoding in advance, which is
exactly what operators of fleet-deployed agents do not know.

The current failure mode is also mostly silent. UTF-16LE ASCII content read
with charset: utf-8 (or with no encoding at all) contains no invalid UTF-8
byte sequences (0x00 is valid UTF-8), so DecoderMalformedReplacement never
fires and NULL-riddled lines ship to sinks. When replacement warnings do fire,
they carry only the encoding name, not the file, so a single bad file in a
large glob is hard to locate.

Attempted Solutions

  • Explicit encoding.charset per source: works only when every file matched by the source shares one encoding, and breaks when a log changes encoding across rotation.
  • Pre-transcoding files outside Vector: not viable for logs being written in place by third-party software.

Proposal

Add an auto mode to the file source's encoding option:

encoding:
  charset: auto            # existing charset labels keep working unchanged
  fallback_charset: utf-8  # optional; used when detection is inconclusive
                           # (default utf-8 = today's behavior)

Detection is per file, at file open, over the first read chunk (a few bytes
suffice for BOM; a bounded window, e.g. up to 1 KiB, for the heuristics), and
deliberately limited to cases that are (nearly) impossible to get wrong:

  1. BOM (authoritative): EF BB BF -> UTF-8, FF FE -> UTF-16LE,
    FE FF -> UTF-16BE (exactly the set encoding_rs::Encoding::for_bom
    recognizes).
  2. BOM-less UTF-16 heuristic: NULL bytes heavily concentrated at one byte
    parity and near-absent at the other, AND the window decodes without errors
    as UTF-16LE/BE (decode_without_bom_handling_and_without_replacement).
  3. Strict UTF-8 validation of the window -> UTF-8.
  4. Otherwise -> fallback_charset, with a once-per-file warning that
    detection was inconclusive.

Ordering note: step 2 must precede step 3, because BOM-less UTF-16LE ASCII is
byte-valid UTF-8; a UTF-8-first ladder can never detect the file class that
motivates this feature.

Non-goals (deliberate):

  • No detection of legacy single-byte codepages (windows-125x etc.): they are
    statistically indistinguishable from each other, and a wrong guess silently
    corrupts data. Operators who know the encoding set an explicit charset (or
    fallback_charset). This also keeps chardetng-style content guessing out.
  • No UTF-32 (not supported by encoding_rs / the Encoding Standard, and
    effectively absent from real log files).

Implementation sketch:

  • Detection lives with per-file state in lib/file-source (FileWatcher),
    because the line delimiter must become per-file: today the delimiter is
    encoded once per source into the configured charset and used for byte-level
    framing. With auto, the watcher sniffs at open, picks the per-file
    delimiter bytes, and annotates emitted Lines with the detected encoding.
  • The source-side transcoding keeps today's decoder-sharing compromise, one
    decoder per detected charset instead of one per source.
  • Rotation/truncation already produces fresh per-file state via
    fingerprinting, so re-detection across rotation (where real logs do change
    encoding) falls out naturally.
  • Observability: a debug-level internal event per file on detection (file,
    detected encoding, via bom|utf16-heuristic|utf8-valid), a warning on
    inconclusive fallback, and (small companion improvement) file attribution on
    DecoderMalformedReplacement, which currently logs only the encoding name.
  • No new dependencies: encoding_rs (already used for transcoding) provides
    BOM sniffing and strict validity checks; UTF-8 validation can use simdutf8
    (already in the tree).

Existing behavior is unchanged unless charset: auto is configured.

I want to open a PR and would like feedback before we implement.

References

Version

vector 0.56.0 (x86_64-pc-windows-msvc 7923556 2026-05-02 05:50:46.918905925)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions