Sensitive content stays on the instance
What a red-log ticket publishes, and the switch that publishes nothing. The platform-wide rule —
what a log line may carry at all — is core's Sensitive Content in Logs
(get Doc/Architecture/SensitiveContentInLogs). This page is the ticketing half: what the incident
filer copies from the logs into GitHub.
What was wrong
An enterprise client's instance holds health records in node content. A call site that formats a
value into its message — Update failed for record {name} — puts that value into the formatted
line, and the console formatter Loki ships has no message template to fall back to: the TEXT is all
there is. The filer then copied that text into three public places:
| Where | What it carried |
|---|---|
| the issue body | the samples verbatim (Recent log lines), the per-shape ledger's normalized excerpt |
| every "still happening" comment, and the retirement note | the last three samples, the shape ledger |
| the site-fold note | a Detail row — the normalized exception message |
Plugins#4444 masked mesh PATHS in all of them, and that
is the limit of masking: a path has a shape, a person's name or a diagnosis is just a word.
LogLineParser.Normalize keeps such a word too — it masks guids, numbers, quoted literals and
paths, nothing that reads as prose — so the normalized text is not a template either.
What is published now
Every text the filer posts — LogIncidentFiler.Body, the recurrence comment, RetirementComment,
SiteFoldNote — carries:
- the fingerprint and the incident node's path, so a person with access can read the lines;
- the logger category, the exception type and the top frame (
Type.Method, no file, no arguments) — the evidence core'sErrorSignatureLoggerProvideralso limits itself to; - counts and the window (occurrences, since the last update, first and last seen), the namespace and pod ledger;
- the shape ledger by key — each shape's key, count and window, without its excerpt
(
LogIncidentCorpusMigration.ShapeEvidence(incident, includeDetail: false)); - Recent sightings: one row per recent sample with its time and pod, and the sentence that the lines are withheld and where they are.
The incident node in the Admin partition keeps the samples verbatim — that is on the instance that recorded them, under its own access control.
The triage draft is not published either — by default
The triage agent reads the incident node, raw samples included: that is how it finds the concrete
case, and it cannot do its job on the template-level fields alone. So whatever it writes can echo a
logged value, and telling it not to (its instructions do) is a request, not a guarantee. The input
cannot be cut without blinding it, so the cut is at the OUTPUT:
| Key | Default | Effect |
|---|---|---|
LogWatch:PublishTriageDraft |
false |
false ⇒ none of the agent's TEXT is published: the issue title is BUILT from the exception's short type and the fault site (InvalidOperationException in MeshDataSource.Apply — the top frame's Type.Method, else the category), the body opens with a line saying the write-up stays on the incident node, the routing row says an override's reason is on the node, and the fold note carries neither the agent's title nor its write-up. The draft's labels and severity still apply, and the write-up stays on the incident node in the Admin partition. true ⇒ the previous behaviour, path-masked: for an instance whose logs carry only its own platform's text. |
Pinned by PublishedIncidentTextCarriesNoLogTextTest.ATriageDraftThatEchoesASample_IsNotPublished_ByDefault:
a marker in a sampled line, echoed into the draft's title, body and routing reason, reaches neither the
issue nor the fold note.
The switch: LogWatch:FileIssues
| Key | Default | Effect |
|---|---|---|
LogWatch:FileIssues |
true |
false ⇒ incidents are still ingested and recorded on this instance, and nothing reaches GitHub: no issue, no recurrence comment, no reopen, no retirement note, whatever GitHub App credentials the instance holds. No triage is asked for either — a draft for a ticket that is never opened is a model round spent on nothing, and a Failed incident would otherwise be re-triaged on every report. A Triage, File or Comment request already PERSISTED on a node (asked before the switch, or by an admin) is consumed at the claim and granted nothing, with the reason on the incident; the comment-debt clock and the stranded-triage reconcile ask for nothing. An admin's Suppress is still honoured. |
The other ways the egress is off, for completeness: LogWatch:IngestToken unset (the ingest endpoint
is not mapped at all), no GitHub App (GitHub:App), or a watcher with no LogWatcher:PortalUrl. Each
is an absence; FileIssues=false is the one that says so on purpose.
The in-cluster watcher sends raw samples to its portal
🚨 The watcher posts its samples to LogWatcher:PortalUrl. Raw lines (up to
LogWatcher:MaxSamplesPerReport × LogWatcher:MaxSampleLength, default 5 × 2000 characters) travel
to whichever portal that names, and are stored there on the incident node. An instance whose logs can
carry sensitive content points its watcher at ITS OWN portal — never at a shared control instance —
and the watcher's LogWatcher:Namespaces lists only that instance's namespace, so no other
deployment's lines are read into it either. None of LogWatch:* above governs this hop: it happens
before the portal sees anything.
Pinned by
PublishedIncidentTextCarriesNoLogTextTest (MeshWeaver.Observability.Test): the issue body, the
recurrence comment, the site-fold note and the retirement note carry none of a logged value while
carrying the category, exception type, top frame, count and pod; FileIssues=false refuses every
GitHub write with credentials present, asks for no triage or comment, consumes a persisted
Triage/File/Comment request at the claim, keeps the comment-debt clock silent, and still grants a
Suppress; a marker in a sampled line echoed into the triage draft reaches neither the issue nor
the fold note.