Skip to content

feat(metrics-emf): warn once when AWS_ENV/ENV missing#2835

Open
abhishekgarg18 wants to merge 2 commits into
mainfrom
fix/aso-metrics-env-fallback-warn
Open

feat(metrics-emf): warn once when AWS_ENV/ENV missing#2835
abhishekgarg18 wants to merge 2 commits into
mainfrom
fix/aso-metrics-env-fallback-warn

Conversation

@abhishekgarg18

@abhishekgarg18 abhishekgarg18 commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Fix a silent metric mis-labeling failure mode. When neither AWS_ENV nor ENV is set on the Lambda, resolveEnvironment returns 'dev' — meaning every prod metric routes to the dev dashboard while prod dashboards read zero traffic.

Today this fails silently: on-call notices the wrong-tier dashboard is unexpectedly noisy hours after a deploy, or worse, doesn't notice because they only watch prod. Making it log-observable so a broken env-var manifest surfaces on the first cold-start invocation.

Change

resolveEnvironment(env, {log}) now emits log.warn once per Lambda instance when the default fallback fires. Rate-limited via a module-scoped latch so a mis-configured deploy doesn't flood the log with one warn per request.

Defensive: skips the warn if log.warn is absent (some helix / Lambda contexts only guarantee log.info and log.error) so the request path never crashes.

Call sites threaded (all 5 that had access to log)

  • src/controllers/redirects.js:144 (ASO overlay)
  • src/controllers/webhooks.js:86, 101 (GitHub webhook enqueue + error handler)
  • src/controllers/brands.js:156 (BrandsController — post-review fix, was missed initially)
  • src/support/aso-overlay-key-handler.js:78
  • src/support/github-webhook-hmac-handler.js:83

Backwards-compatible for legacy callers that pass no options — return value and precedence rules unchanged.

Design decisions

  • Module-scoped latch (not per-request): a mis-configured deploy shouldn't flood the log with one warn per request. The Node/Lambda execution model resets the module on cold start, so the warn fires exactly once per Lambda instance — enough for ops to see it on the first invocation without noise thereafter.
  • Test-only reset export (resetEnvFallbackWarnedForTest): required to isolate the latch across unit tests without cross-test bleed. Named to make its scope obvious — no production caller should reach for it.
  • typeof log.warn === 'function' guard: repo has no shared log-shape convention (grep confirms), and helix contexts historically only guarantee log.info. Best-effort path must never throw.

Origin

Follow-up to a post-review audit on #2832. Split out here because the concern is orthogonal to the overlay-tier fix in that PR — this hardens metric labeling repo-wide.

Test plan

  • Unit tests: warn suppressed when AWS_ENV set, warn suppressed when ENV set, warn fires exactly once when both missing, no-op when log absent, warn skipped when log.warn is undefined
  • All 5 controllers/handlers that use resolveEnvironment thread log through
  • 404 tests pass across brands, redirects, webhooks, github-webhook-hmac
  • Lint clean, type-check clean
  • Post-deploy: verify no warn fires in dev/stage/prod (env-var manifest is correct in all three today; the warn is only expected to fire if someone regresses the manifest)

Related

🤖 Generated with Claude Code

When neither `AWS_ENV` nor `ENV` is set on the Lambda, `resolveEnvironment`
silently returns `'dev'` — meaning every metric routes to the dev dashboard
while prod dashboards read zero traffic. That's a P1-shaped failure mode with
no observable signal today: on-call notices the wrong-tier dashboard is
suddenly noisy hours after a deploy, or worse, doesn't notice at all because
they only watch prod.

Change `resolveEnvironment(env, {log})` to log.warn once per Lambda instance
when the default fallback fires, so a broken env-var manifest surfaces on the
first cold-start invocation. Rate-limited via a module-scoped latch so a
mis-configured deploy doesn't flood the log with one warn per request.

Defensive: skip the warn if `log.warn` is absent (some helix / Lambda
contexts only guarantee `log.info` and `log.error`) so the request path never
crashes.

Callers threaded through:
- src/controllers/redirects.js (ASO overlay)
- src/controllers/webhooks.js (GitHub webhook enqueue + errorHandler)
- src/support/aso-overlay-key-handler.js
- src/support/github-webhook-hmac-handler.js

Backwards-compatible for legacy callers that pass no options — return value
and precedence rules unchanged. Test-only reset export so the warn-once latch
doesn't cross-contaminate tests.

Follow-up to PR #2832 post-review audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…ent call

Post-review finding on this PR: I missed a 5th call site — `BrandsController`
emits `BrandDemotionBlocked` metrics and would fall back to `'dev'` silently
if the Lambda's env-var manifest dropped AWS_ENV/ENV. Threading `log`
through here means all metric-emitting controllers now surface the mis-config
warning on cold start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@abhishekgarg18 abhishekgarg18 changed the title observability(metrics-emf): warn once when AWS_ENV/ENV missing chore(metrics-emf): warn once when AWS_ENV/ENV missing Jul 16, 2026
@github-actions

Copy link
Copy Markdown

This PR will trigger no release when merged.

@abhishekgarg18 abhishekgarg18 changed the title chore(metrics-emf): warn once when AWS_ENV/ENV missing feat(metrics-emf): warn once when AWS_ENV/ENV missing Jul 16, 2026
miakobchuk added a commit that referenced this pull request Jul 16, 2026
…2832)

> **Defense-in-depth positioning** (2026-07-16). Cornel Isbiceanu
(Skyline dispatcher team) noted on Slack that in steady state preview
pods aren't supposed to be polling this endpoint — the ASO API key is
only injected in publish pods, not preview. This PR is *not* meant to
normalize preview traffic; it's meant to make api-service resilient to
whatever URL shape the dispatcher sends. Rationale:
>
> 1. **Auth is still required.** Accepting the `-prev` URL shape at the
route regex does not weaken auth: `AsoOverlayKeyHandler` still requires
a valid `X-ASO-API-Key` regardless of URL shape. Preview pods without
the key are still rejected — just as 401 from the origin handler instead
of Fastly edge, or 401 from Fastly edge for canonical URLs. Same
posture, different layer.
> 2. **Fastly edge remains the primary signal.** Skyline's diagnostic
surface for "preview pods are misconfigured" is the Fastly edge log (401
rate by URL). That signal is unchanged by this PR — we only affect what
happens *if* a request reaches origin. Preview pods still get 401'd at
edge today because they lack the key; that alarm still fires.
> 3. **Zero-cost when preview eventually gets the key.** If Skyline /
AEM CS ever provision the API key on preview pods (planned or not), this
code just works with no follow-up patch. Symmetric to how we already
handle the same overlay across dev/stage/prod tiers.
> 4. **Future-proof against dispatcher changes.** If Skyline's URL
construction changes to strip `-prev` upstream, this PR becomes a no-op.
If it doesn't, origin behavior is deterministic instead of a shape-based
400.
>
> This is a resilience-hardening PR, not a functional change.

## Summary

AEM Cloud Service preview publish pods send `AEM_SERVICE=cm-pN-eN-prev`.
Skyline dispatcher builds the overlay URL from that env var verbatim, so
preview traffic hits us with `/config/cm-pN-eN-prev/redirects.txt`. Two
api-service regexes reject the shape today:

1. `AsoOverlayKeyHandler.OVERLAY_ROUTE` does not match `-prev`, so the
request falls through the auth chain and 401s at `authWrapper` before
the controller runs.
2. Even with correct edge auth, `RedirectsController.SERVICE_RE` would
400 the shape.

Mystique writes exactly one overlay per (program, environment) — publish
and preview tiers share it. This PR accepts the suffix at both
boundaries and strips it before all downstream lookups (Site lookup, S3
key, Surrogate-Key). Origin-side stripping avoids requiring a Fastly VCL
change and lands the fix on our side alone.

**Auth model is preserved end-to-end.** `AsoOverlayKeyHandler` still
requires `X-ASO-API-Key` regardless of URL shape — accepting `-prev` at
the route regex does not weaken any authentication guarantee. A future
auth-chain regression that accidentally auto-allowed keyless
overlay-route requests would be the actual security problem, and that
risk exists for `cm-pN-eN` as much as for `cm-pN-eN-prev`.

## Fastly cache footprint

Fastly caches on raw URL, so preview and publish create two separate
cache entries per tenant. Trade-off is acceptable because:

1. Both entries share `Surrogate-Key: aso-overlay-<canonical>`, so
mystique's purge-on-Deploy invalidates both in a single call — no
divergence risk.
2. Both fetch the same S3 object (canonical), so upstream is not doubled
beyond initial per-URL fills — subsequent polls are cache hits.
3. At 10k tenants × ~20% preview coverage the extra entries are cheap.
4. No cache-poisoning risk — publish and preview have distinct cache
keys.

A future normalization at Fastly VCL (`regsub` in `vcl_recv` before
cache-key derivation) would collapse to one entry per tenant, but that
requires Fastly-admin territory. Origin-side is the pragmatic bridge.

## Changes

- `SERVICE_RE` (`src/controllers/redirects.js:43`): accept optional
trailing `-prev`, case-sensitive
- `RedirectsController.getRedirects`: derive `canonicalService` from
captured program/environment ids; use it for S3 key + Surrogate-Key on
both 200 and 304 branches
- `AsoOverlayKeyHandler.OVERLAY_ROUTE`
(`src/support/aso-overlay-key-handler.js:33`): accept `-prev` so the
auth handler doesn't 401 preview traffic before it reaches the
controller
- **`s3DurationEmitted` idempotency guard** (audit follow-up): fix
double-emission of `AsoOverlayS3ReadDurationMs` when the body stream
throws post-`send()`. Test in `redirects.test.js` asserts exactly one
emission with SUCCESS result (first-writer-wins).
- **OpenAPI `Surrogate-Key` documentation** (audit follow-up): document
the header on both 200 and 304 response schemas so the mystique
purge-on-Deploy contract is discoverable
- Unit tests: `-prev` happy path, 304 on `-prev`, malformed variants
still 400 (`-preview`, `-PREV`, `-prev-2`, trailing hyphen), auth
handler accepts `-prev`
- IT test: `-prev` end-to-end against the seeded canonical overlay
- OpenAPI: pattern + description updated to document optional `-prev`

Jira: [SITES-48140](https://jira.corp.adobe.com/browse/SITES-48140)

## Test plan

- [x] 58 unit tests in `redirects.test.js` pass, all auth handler tests
green
- [x] Full suite green (2 pre-existing flakes unrelated to this PR; pass
in isolation)
- [x] Lint + type-check clean
- [x] OpenAPI validates + docs rebuilt
- [ ] Post-deploy: verify `-prev` URL returns the same canonical body +
`surrogate-key: aso-overlay-cm-pN-eN` header via curl on dev
- [ ] Post-deploy: verify Fastly edge 401 rate for `-prev` URLs is
unchanged (the edge signal for upstream misconfig is preserved)

## Post-review audit findings

Self-review by staff-engineer-level reviewer flagged:
- **I1 (Important):** double-emit of `AsoOverlayS3ReadDurationMs` —
**fixed in this PR** via `s3DurationEmitted` guard
- **I4 (Important):** OpenAPI missing `Surrogate-Key` doc — **fixed in
this PR**
- **I3 (Important):** `resolveEnvironment` silent fallback — **split out
to #2835** (independent concern)
- **I2 + M1-M5 (Minor):** tracked as follow-up tasks; not blocking merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Abhishek Garg <abhigarg+adobe@adobe.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: miakobchuk <miakobchuk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant