Scrubbing Logs Before You Send Them to an AI
AI review of support logs is genuinely useful. A model can read ten thousand lines faster than I can, spot the pattern I would have found on page forty, and hand me a summary I can check. The problem is what a raw support bundle contains: hostnames, vCenter names, VM names, IP addresses, UUIDs, MoRefs, MAC addresses, email addresses, datastore names. Send the raw bundle and you have handed all of that to a third party. So the rule in my lab is simple: nothing raw leaves the machine. Scrub locally, let the AI read sanitized text only, unscrub the answer locally, verify before anything ships.
The scrubber is a stdlib-only Python script. It walks a support bundle, file by file, and replaces every identifier it recognizes with a synthetic placeholder, applied consistently across the whole bundle. A hostname becomes hostN, a domain becomes domainN, a UUID becomes uuidN, a user becomes userN@domainN. Private IPs map into 10.100.x.x, public IPs into the TEST-NET-3 range 203.0.113.x, MACs into a synthetic range. Consistency is the whole point: if the same disk UUID appears in vmkernel.log, in a hostd log, and in a vSAN trace, it gets the same placeholder in all three, so the logs stay readable and cross-references still work after scrubbing.
The mapping between placeholders and originals is the reversal key, and it is treated like one. It is written next to the output, never inside it, and it never goes to the AI, to a customer, or anywhere else. A scrubbed bundle without its mapping is anonymous. A mapping file in the wrong hands undoes all of it. That file is also why the script's output folder is never the thing you share: the output is a directory tree of scrubbed files, and the mapping sits beside it.
Scale is worth stating plainly, because it is the reason this had to be a script and not careful find-and-replace. The first full bundle I ran through it was 3,544 files. The run made roughly 476 million replacements and produced a mapping of about 3.8 million entries. Phase 1 took about five and a half minutes; Phase 2 took about 110 minutes on 16 workers. That run finished only because nothing changed under it mid-run. An earlier run had fifty files change during scrubbing, which kicked off a single-threaded fallback that would have taken most of a day. Changed files during a run are a real failure mode, not a theoretical one.
The first completed run also taught me that scrubbing is not the same as verifying. The run finished with 18 files carrying 2,040 leftover FQDN hits. Leftovers print as per-file counts only, and they get reviewed by hand, because there are three kinds of leftover and only one of them is a leak: plain noise (NSX syslogs are the worst offender), tokens derived from synthetic IPs the scrubber itself created, and genuinely different tokens that need a second look. I built a small classifier for this that prints aggregate patterns only, never a full token or a real value. The point of the classifier is to sort the noise from the real questions without creating a second leak in the process of checking for the first one.
Verification itself is a method, not a feeling. First, an exact-value sweep: take every hostname, domain, user, VM, vCenter, email, datastore name, and MoRef number from the mapping and search the scrubbed output for the originals. Expect zero hits. Then shape sweeps: UUID shapes should return zero; IPv4 addresses should be only synthetic ranges; email addresses should be only synthetic userN@domainN forms; MACs should resolve to synthetic mapping entries; naa, eui, and t10 identifiers should be synthetic only; FQDNs should be hostN.domainN forms except for leftovers already adjudicated. After zero unexplained hits, the defensible claim is narrow and exact: no real identifiers found in contents by exact and shape sweep, leftovers adjudicated, on this bundle. That is not "foolproof," and I will not call it that.
The honest gaps matter as much as the passes. The scrubber works on file contents only. File paths and archive member names are not scrubbed yet, so a path that carries a lab name survives. Identifier formats the script has never seen will survive too, and archives it cannot expand pass through untouched. That is why the standing rule is to send selected evidence files, never the whole tree. A scrubbed bundle is safer than a raw one, not safe in any absolute sense.
One more thing the build taught me, and it is the reason I trust the current script more than the first version: testing caught a real bug. An optimization reordered the work queue, and the first version of the code zipped worker results against the file list in the old order, renaming scrubbed temp files onto the wrong originals. The fix was to pair results against the scrub order, and the proof was byte-identical output across the old script, the new script in parallel, and the new script single-threaded, plus identical mapping files. Verify the output before you deploy the tool. That applies to the scrubber exactly the way it applies to everything else.
The workflow, end to end: scrub locally, AI reviews sanitized text only, unscrub the answer locally, verify, then ship. Nothing raw crosses the boundary in either direction.