You are implementing Task A7: add the `redact_emails_traces` remap transform to the Vector helm values template, plus a VRL unit test. This walks span attributes AND resource.attributes.

## What's already in place

- Working directory: `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector`
- `templates/values.yaml.tpl` has sources, drop_audit filter, redact_emails_logs remap, sinks. The `datadog_traces` sink currently reads directly from `otlp_in.traces`.
- `tests/vrl/drop_audit/` and `tests/vrl/redact_emails_logs/` are passing.
- `vector` 0.55.0 CLI installed at `/opt/homebrew/bin/vector`. `jq` installed.

## Vector 0.55 VRL lessons (from prior tasks — apply these proactively)

1. **Path access is infallible.** `.path ?? "x"` is rejected with E651. Use `if is_string(.path) { string!(.path) } else { "" }` or equivalent type guard.
2. **Local-variable type narrowing required for `map_values` lambdas.** Direct `map_values(object!(.attributes))` chained with `replace(v, ...)` inside the lambda fails type checking. Use this pattern instead:

   ```vrl
   if is_object(.attributes) {
     attrs, _err = object(.attributes)
     if is_object(attrs) {
       .attributes = map_values(object!(attrs)) -> |v| {
         if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
       }
     }
   }
   ```

   Note: inside the `is_string(v)` guard, use `string!(v)` to narrow `v`'s type for `replace()`.

3. **Runner pattern:** `vector vrl -p program.vrl -i inputs.ndjson -q -o > .actual.ndjson`, then diff via `jq -c .` normalization. NOT `--no-prompt` (does not exist in 0.55).

## Task Description (verbatim from plan, with VRL/runner fixes baked in)

### Task A7: redact_emails_traces remap + VRL unit test

**Files:**
- Modify: `templates/values.yaml.tpl`
- Create: `tests/vrl/redact_emails_traces/program.vrl`
- Create: `tests/vrl/redact_emails_traces/inputs.ndjson`
- Create: `tests/vrl/redact_emails_traces/expected.ndjson`
- Create: `tests/vrl/redact_emails_traces/run.sh`

The OTLP-source span shape exposes `.attributes` for span-level attributes and `.resource.attributes` for resource-level. Walk both.

- [ ] **Step 1: VRL program** at `tests/vrl/redact_emails_traces/program.vrl`. Use the type-narrowing pattern proven in A6:

```vrl
email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}

if is_object(.resource) {
  res, _err = object(.resource)
  if is_object(res) && is_object(res.attributes) {
    rattrs, _err2 = object(res.attributes)
    if is_object(rattrs) {
      .resource.attributes = map_values(object!(rattrs)) -> |v| {
        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
      }
    }
  }
}

.
```

If a different idiom for safely descending into `.resource.attributes` works in your installed Vector, prefer it (simpler is better). Iterate until the test passes.

- [ ] **Step 2: Inputs** at `tests/vrl/redact_emails_traces/inputs.ndjson`:

```json
{"name": "POST /signup", "attributes": {"user.email": "new@x.com", "http.status": 200}, "resource": {"attributes": {"service.name": "web", "owner": "team@x.com"}}}
{"name": "GET /health", "attributes": {"http.status": 200}, "resource": {"attributes": {"service.name": "web"}}}
```

- [ ] **Step 3: Expected outputs** at `tests/vrl/redact_emails_traces/expected.ndjson`:

```json
{"name": "POST /signup", "attributes": {"user.email": "[REDACTED-EMAIL]", "http.status": 200}, "resource": {"attributes": {"service.name": "web", "owner": "[REDACTED-EMAIL]"}}}
{"name": "GET /health", "attributes": {"http.status": 200}, "resource": {"attributes": {"service.name": "web"}}}
```

- [ ] **Step 4: Runner** at `tests/vrl/redact_emails_traces/run.sh`:

```bash
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACTUAL="$DIR/.actual.ndjson"
vector vrl -p "$DIR/program.vrl" -i "$DIR/inputs.ndjson" -q -o > "$ACTUAL"
diff -u <(jq -c . "$DIR/expected.ndjson") <(jq -c . "$ACTUAL")
echo "redact_emails_traces VRL OK"
```

`chmod +x tests/vrl/redact_emails_traces/run.sh`.

- [ ] **Step 5: Run, expect PASS.**

```bash
./tests/vrl/redact_emails_traces/run.sh
```

If the VRL fails to compile, debug iteratively. Stick to the proven type-narrowing patterns from A6.

- [ ] **Step 6: Add transform to `templates/values.yaml.tpl`.** Append into the existing `transforms:` map (after `redact_emails_logs:`):

```yaml
    redact_emails_traces:
      type: remap
      inputs:
        - otlp_in.traces
      source: |
        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
        if is_object(.attributes) {
          attrs, _err = object(.attributes)
          if is_object(attrs) {
            .attributes = map_values(object!(attrs)) -> |v| {
              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
            }
          }
        }
        if is_object(.resource) {
          res, _err = object(.resource)
          if is_object(res) && is_object(res.attributes) {
            rattrs, _err2 = object(res.attributes)
            if is_object(rattrs) {
              .resource.attributes = map_values(object!(rattrs)) -> |v| {
                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
              }
            }
          }
        }
```

(If your VRL program in Step 1 uses a slightly different working idiom, mirror that idiom in the template — keep them in sync.)

NOTE: `${email_redaction_replacement}` is a Terraform `templatefile()` variable, not a literal.

- [ ] **Step 7: Update traces sink** in `templates/values.yaml.tpl`:

```yaml
    datadog_traces:
      type: datadog_traces
      inputs:
        - redact_emails_traces   # was: otlp_in.traces
```

- [ ] **Step 8: Run `terraform fmt -check -recursive`** — expected exit 0.

## Constraints

- **DO NOT run any git commands.** No `git add`, no `git commit`. The user handles git manually.
- **DO NOT** push, tag, open MRs.
- **DO NOT** run `terraform apply`, `kubectl`, `helm install`, anything that mutates infra.
- **DO NOT** create snapshot-test infrastructure (`tests/snapshots/` is OFF the table).
- VRL test execution via `vector` CLI is required.

## Spec / plan references

- Spec: `/Users/steve.briggs/projects/IAC/docs/superpowers/specs/2026-05-05-vector-redaction-design.md` §4.3
- Plan: `/Users/steve.briggs/projects/IAC/docs/superpowers/plans/2026-05-05-vector-redaction-pipeline.md` Task A7

## Report Format

- **Status:** DONE | DONE_WITH_CONCERNS | BLOCKED | NEEDS_CONTEXT
- VRL test result (must show "redact_emails_traces VRL OK")
- Template changes (lines modified)
- `terraform fmt` result
- Any divergence from the literal VRL pattern in the plan (and why)
- Self-review findings


I'll start by reading the existing files to understand the current state before making changes.


Bash

List existing VRL test directories


1	role: Aggregator
2	replicas: ${replicas}
3	podAnnotations:
4	  reloader.stakater.com/auto: "true"
5	service:
6	  enabled: true
7	  type: ClusterIP
8	  ports:
9	    - name: otlp-grpc
10	      port: 4317
11	      protocol: TCP
12	    - name: otlp-http
13	      port: 4318
14	      protocol: TCP
15	env:
16	  - name: DATADOG_API_KEY
17	    valueFrom:
18	      secretKeyRef:
19	        name: vector-datadog
20	        key: api_key
21	resources:
22	  requests:
23	    cpu: ${requests_cpu}
24	    memory: ${requests_memory}
25	  limits:
26	    cpu: "${limits_cpu}"
27	    memory: ${limits_memory}
28	customConfig:
29	  data_dir: /vector-data-dir
30	  api:
31	    enabled: true
32	    address: 127.0.0.1:8686
33	  sources:
34	    otlp_in:
35	      type: opentelemetry
36	      grpc:
37	        address: 0.0.0.0:4317
38	      http:
39	        address: 0.0.0.0:4318
40	  transforms:
41	    drop_audit:
42	      type: filter
43	      inputs:
44	        - otlp_in.logs
45	      condition: |
46	        msg = if is_string(.message) { string!(.message) } else { "" }
47	        !match(msg, r'Audit\s-\s.+') && !match(msg, r'"level":"audit"')
48	    redact_emails_logs:
49	      type: remap
50	      inputs:
51	        - drop_audit
52	      source: |
53	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
54	        if is_string(.message) {
55	          .message = replace(string!(.message), email_re, "${email_redaction_replacement}")
56	        }
57	        if is_object(.attributes) {
58	          attrs, _err = object(.attributes)
59	          if is_object(attrs) {
60	            .attributes = map_values(object!(attrs)) -> |v| {
61	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
62	            }
63	          }
64	        }
65	  sinks:
66	    datadog_logs:
67	      type: datadog_logs
68	      inputs:
69	        - redact_emails_logs
70	      default_api_key: $${DATADOG_API_KEY}
71	      site: ${datadog_site}
72	    datadog_traces:
73	      type: datadog_traces
74	      inputs:
75	        - otlp_in.traces
76	      default_api_key: $${DATADOG_API_KEY}
77	      site: ${datadog_site}
78	    datadog_metrics:
79	      type: datadog_metrics
80	      inputs:
81	        - otlp_in.metrics
82	      default_api_key: $${DATADOG_API_KEY}
83	      site: ${datadog_site}
84


drwxr-xr-x steve.briggs staff 128 B Tue May  5 12:52:54 2026 ..
drwxr-xr-x steve.briggs staff 224 B Tue May  5 12:59:12 2026 drop_audit
drwxr-xr-x steve.briggs staff 128 B Tue May  5 13:01:16 2026 .
drwxr-xr-x steve.briggs staff 224 B Tue May  5 13:08:23 2026 redact_emails_logs


Good. Now let me look at the existing redact_emails_logs test to understand the pattern, then create the new test files.


List redact_emails_logs test files


drwxr-xr-x steve.briggs staff 128 B Tue May  5 13:01:16 2026 ..
.rw-r--r-- steve.briggs staff 273 B Tue May  5 13:01:33 2026 inputs.ndjson
.rwxr-xr-x steve.briggs staff 290 B Tue May  5 13:02:25 2026 run.sh
.rw-r--r-- steve.briggs staff 390 B Tue May  5 13:05:41 2026 program.vrl
.rw-r--r-- steve.briggs staff 289 B Tue May  5 13:08:23 2026 expected.ndjson
drwxr-xr-x steve.briggs staff 224 B Tue May  5 13:08:23 2026 .
.rw-r--r-- steve.briggs staff 301 B Tue May  5 13:13:35 2026 .actual.ndjson


1	email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
2
3	if is_string(.message) {
4	  .message = replace(string!(.message), email_re, "[REDACTED-EMAIL]")
5	}
6
7	if is_object(.attributes) {
8	  attrs, _err = object(.attributes)
9	  if is_object(attrs) {
10	    .attributes = map_values(object!(attrs)) -> |v| {
11	      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
12	    }
13	  }
14	}
15	.
16


1	#!/usr/bin/env bash
2	set -euo pipefail
3	DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
4	ACTUAL="$DIR/.actual.ndjson"
5	vector vrl -p "$DIR/program.vrl" -i "$DIR/inputs.ndjson" -q -o > "$ACTUAL"
6	diff -u <(jq -c . "$DIR/expected.ndjson") <(jq -c . "$ACTUAL")
7	echo "redact_emails_logs VRL OK"
8


Good. I have all the context I need. Now I'll create the test directory and files.


Create redact_emails_traces test directory


(Bash completed with no output)


Write

email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'

if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| {
      if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
    }
  }
}

if is_object(.resource) {
  res, _err = object(.resource)
  if is_object(res) && is_object(res.attributes) {
    rattrs, _err2 = object(res.attributes)
    if is_object(rattrs) {
      .resource.attributes = map_values(object!(rattrs)) -> |v| {
        if is_string(v) { replace(string!(v), email_re, "[REDACTED-EMAIL]") } else { v }
      }
    }
  }
}

.


File created successfully at: /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/program.vrl (file state is current in your context — no need to Read it back)


{"name": "POST /signup", "attributes": {"user.email": "new@x.com", "http.status": 200}, "resource": {"attributes": {"service.name": "web", "owner": "team@x.com"}}}
{"name": "GET /health", "attributes": {"http.status": 200}, "resource": {"attributes": {"service.name": "web"}}}


File created successfully at: /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/inputs.ndjson (file state is current in your context — no need to Read it back)


{"name": "POST /signup", "attributes": {"user.email": "[REDACTED-EMAIL]", "http.status": 200}, "resource": {"attributes": {"service.name": "web", "owner": "[REDACTED-EMAIL]"}}}
{"name": "GET /health", "attributes": {"http.status": 200}, "resource": {"attributes": {"service.name": "web"}}}


File created successfully at: /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/expected.ndjson (file state is current in your context — no need to Read it back)


#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ACTUAL="$DIR/.actual.ndjson"
vector vrl -p "$DIR/program.vrl" -i "$DIR/inputs.ndjson" -q -o > "$ACTUAL"
diff -u <(jq -c . "$DIR/expected.ndjson") <(jq -c . "$ACTUAL")
echo "redact_emails_traces VRL OK"


File created successfully at: /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/run.sh (file state is current in your context — no need to Read it back)


Bash

Make run.sh executable


Now run the test to verify before touching the template:


Run the VRL unit test


Error: Exit code 1
2026-05-05T17:16:12.318831Z  INFO vector::app: Log level is enabled. level="info"
--- /dev/fd/63	2026-05-05 13:16:12
+++ /dev/fd/62	2026-05-05 13:16:12
@@ -1,2 +1,2 @@
-{"name":"POST /signup","attributes":{"user.email":"[REDACTED-EMAIL]","http.status":200},"resource":{"attributes":{"service.name":"web","owner":"[REDACTED-EMAIL]"}}}
-{"name":"GET /health","attributes":{"http.status":200},"resource":{"attributes":{"service.name":"web"}}}
+{"attributes":{"http.status":200,"user.email":"[REDACTED-EMAIL]"},"name":"POST /signup","resource":{"attributes":{"owner":"[REDACTED-EMAIL]","service.name":"web"}}}
+{"attributes":{"http.status":200},"name":"GET /health","resource":{"attributes":{"service.name":"web"}}}


The VRL program logic is correct — redaction is working perfectly. The only diff is key ordering in the JSON output (jq sorts keys alphabetically in actual output). The expected file needs to use the same sort order. Let me fix the expected file:


See what jq normalizes the expected output to


The diff shows jq sorts the actual output keys alphabetically (`attributes` before `name`, `http.status` before `user.email`, etc.) but the expected file doesn't match that sort. The run.sh uses `jq -c .` on both sides of the diff, so the expected file should also be sorted by jq. Since `jq -c .` sorts object keys... wait, actually jq does NOT sort object keys by default — it preserves insertion order. But the actual output from vector has a different key order than the input.

Let me check what the actual output looks like and update expected to match:


Check actual vector output key order


{ "attributes": { "http.status": 200, "user.email": "[REDACTED-EMAIL]" }, "name": "POST /signup", "resource": { "attributes": { "owner": "[REDACTED-EMAIL]", "service.name": "web" } } }
{ "attributes": { "http.status": 200 }, "name": "GET /health", "resource": { "attributes": { "service.name": "web" } } }


Vector sorts keys alphabetically in its output. The expected file needs to match that key order. Let me update expected.ndjson to use the same key ordering:


Write

{"attributes": {"http.status": 200, "user.email": "[REDACTED-EMAIL]"}, "name": "POST /signup", "resource": {"attributes": {"owner": "[REDACTED-EMAIL]", "service.name": "web"}}}
{"attributes": {"http.status": 200}, "name": "GET /health", "resource": {"attributes": {"service.name": "web"}}}


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/tests/vrl/redact_emails_traces/expected.ndjson has been updated successfully. (file state is current in your context — no need to Read it back)


Bash

Run the VRL unit test again


2026-05-05T17:17:17.675770Z  INFO vector::app: Log level is enabled. level="info"
redact_emails_traces VRL OK


Test passes. Now update `templates/values.yaml.tpl` — add the `redact_emails_traces` transform and update the traces sink input:


Edit

  sinks:
    datadog_logs:
      type: datadog_logs
      inputs:
        - redact_emails_logs
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}
    datadog_traces:
      type: datadog_traces
      inputs:
        - otlp_in.traces
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}

    redact_emails_traces:
      type: remap
      inputs:
        - otlp_in.traces
      source: |
        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
        if is_object(.attributes) {
          attrs, _err = object(.attributes)
          if is_object(attrs) {
            .attributes = map_values(object!(attrs)) -> |v| {
              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
            }
          }
        }
        if is_object(.resource) {
          res, _err = object(.resource)
          if is_object(res) && is_object(res.attributes) {
            rattrs, _err2 = object(res.attributes)
            if is_object(rattrs) {
              .resource.attributes = map_values(object!(rattrs)) -> |v| {
                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
              }
            }
          }
        }
  sinks:
    datadog_logs:
      type: datadog_logs
      inputs:
        - redact_emails_logs
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}
    datadog_traces:
      type: datadog_traces
      inputs:
        - redact_emails_traces
      default_api_key: $${DATADOG_API_KEY}
      site: ${datadog_site}


The file /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/templates/values.yaml.tpl has been updated successfully. (file state is current in your context — no need to Read it back)


Now run `terraform fmt -check -recursive` from the module root:


Bash

terraform fmt -check -recursive /Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/

Run terraform fmt check on the module


Exit 0. Let me do a final sanity check on the template to make sure it looks right:


1	role: Aggregator
2	replicas: ${replicas}
3	podAnnotations:
4	  reloader.stakater.com/auto: "true"
5	service:
6	  enabled: true
7	  type: ClusterIP
8	  ports:
9	    - name: otlp-grpc
10	      port: 4317
11	      protocol: TCP
12	    - name: otlp-http
13	      port: 4318
14	      protocol: TCP
15	env:
16	  - name: DATADOG_API_KEY
17	    valueFrom:
18	      secretKeyRef:
19	        name: vector-datadog
20	        key: api_key
21	resources:
22	  requests:
23	    cpu: ${requests_cpu}
24	    memory: ${requests_memory}
25	  limits:
26	    cpu: "${limits_cpu}"
27	    memory: ${limits_memory}
28	customConfig:
29	  data_dir: /vector-data-dir
30	  api:
31	    enabled: true
32	    address: 127.0.0.1:8686
33	  sources:
34	    otlp_in:
35	      type: opentelemetry
36	      grpc:
37	        address: 0.0.0.0:4317
38	      http:
39	        address: 0.0.0.0:4318
40	  transforms:
41	    drop_audit:
42	      type: filter
43	      inputs:
44	        - otlp_in.logs
45	      condition: |
46	        msg = if is_string(.message) { string!(.message) } else { "" }
47	        !match(msg, r'Audit\s-\s.+') && !match(msg, r'"level":"audit"')
48	    redact_emails_logs:
49	      type: remap
50	      inputs:
51	        - drop_audit
52	      source: |
53	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
54	        if is_string(.message) {
55	          .message = replace(string!(.message), email_re, "${email_redaction_replacement}")
56	        }
57	        if is_object(.attributes) {
58	          attrs, _err = object(.attributes)
59	          if is_object(attrs) {
60	            .attributes = map_values(object!(attrs)) -> |v| {
61	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
62	            }
63	          }
64	        }
65	    redact_emails_traces:
66	      type: remap
67	      inputs:
68	        - otlp_in.traces
69	      source: |
70	        email_re = r'[\w._%+\-]+@[\w.-]+\.[A-Za-z]{2,}'
71	        if is_object(.attributes) {
72	          attrs, _err = object(.attributes)
73	          if is_object(attrs) {
74	            .attributes = map_values(object!(attrs)) -> |v| {
75	              if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
76	            }
77	          }
78	        }
79	        if is_object(.resource) {
80	          res, _err = object(.resource)
81	          if is_object(res) && is_object(res.attributes) {
82	            rattrs, _err2 = object(res.attributes)
83	            if is_object(rattrs) {
84	              .resource.attributes = map_values(object!(rattrs)) -> |v| {
85	                if is_string(v) { replace(string!(v), email_re, "${email_redaction_replacement}") } else { v }
86	              }
87	            }
88	          }
89	        }
90	  sinks:
91	    datadog_logs:
92	      type: datadog_logs
93	      inputs:
94	        - redact_emails_logs
95	      default_api_key: $${DATADOG_API_KEY}
96	      site: ${datadog_site}
97	    datadog_traces:
98	      type: datadog_traces
99	      inputs:
100	        - redact_emails_traces
101	      default_api_key: $${DATADOG_API_KEY}
102	      site: ${datadog_site}
103	    datadog_metrics:
104	      type: datadog_metrics
105	      inputs:
106	        - otlp_in.metrics
107	      default_api_key: $${DATADOG_API_KEY}
108	      site: ${datadog_site}
109


Everything looks correct. Here is the report, Sir Charles of St Cheese.

---

**Status:** DONE

**VRL test result:**
```
redact_emails_traces VRL OK
```
Both span-level `attributes` and `resource.attributes` are correctly redacted. The no-email span passes through untouched.

**Template changes (`templates/values.yaml.tpl`):**
- Lines 65–89: Added `redact_emails_traces` remap transform under `transforms:`, reading from `otlp_in.traces`, walking `.attributes` and `.resource.attributes` with the proven type-narrowing pattern. Uses `${email_redaction_replacement}` Terraform templatefile variable.
- Line 100: Updated `datadog_traces` sink input from `otlp_in.traces` to `redact_emails_traces`.

**`terraform fmt` result:** Exit 0, no formatting issues.

**Divergence from plan VRL pattern:** None in logic. One minor divergence: the `expected.ndjson` file uses Vector's alphabetical key-sort order (the actual output order from `vector vrl`) rather than input insertion order. This is required for the `jq -c .` diff to match — both sides of the diff normalize through `jq`, and since Vector emits keys alphabetically, the expected file must reflect that. The program.vrl itself is identical to the plan.

**Self-review findings:**
- The `source:` block in the template does not end with a trailing `.` (the standalone dot that returns the event). This mirrors what `redact_emails_logs` does in the template — both omit it — and Vector's remap transform implicitly returns the mutated event, so this is fine and consistent.
- The VRL program file itself includes the trailing `.` (required for the standalone `vector vrl` runner which needs an explicit return value).
- No other issues found.