Skip to content

feat(observability): send metrics and logs to Grafana Cloud - #399

Open
nourshoreibah wants to merge 4 commits into
mainfrom
worktree-grafana-otel-telemetry
Open

feat(observability): send metrics and logs to Grafana Cloud#399
nourshoreibah wants to merge 4 commits into
mainfrom
worktree-grafana-otel-telemetry

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Why

The lambdas have carried OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS since 2dacc6d, but nothing in the repo ever read them — so the Grafana Cloud account has been receiving nothing. Observability today is Sentry (uncaught throws) plus unstructured console.* in CloudWatch, which can't answer "is the API slow", "which endpoint is erroring", or "how long does a report take".

What

A new file:-linked package, @branch/lambda-telemetry, exporting OTLP metrics and structured logs — wired into the two places every request already passes through, so all six services are covered without per-service work.

dispatch() now records, for every route in every service:

Metric Kind Labels
http.server.requests counter method, route, status
http.server.request.duration histogram (ms) method, route, status
faas.cold_starts counter
http.server.auth_failures counter method, route, unauthenticated | forbidden
http.server.unhandled_errors counter method, route
db.client.operation.duration histogram (ms) statement kind, outcome

The last one comes from log: kyselyTelemetryLog on each lambda's Kysely instance, which also logs slow (>500ms) and failed queries.

Domain metrics, recorded explicitly from controllers:

  • branch.auth.logins — a failure spike is the credential-stuffing signal
  • branch.auth.registrationsinvitation_required shows people bouncing off the invite gate
  • branch.users.invited
  • branch.projects.changed — deletes cascade into expenditures, reports and S3
  • branch.expenditures.changed / .amount — the approval funnel and spend by category
  • branch.donations.recorded / .amount
  • branch.reports.generated / .generation.duration / .size — the slowest path in the backend, nearest the 30s timeout

Logs go to stdout as JSON (CloudWatch unchanged) and to Loki, carrying request id, service, route, status, duration, cold-start flag and caller. serverError() routes through the same logger, so every 500 is structured; an Error is flattened rather than serialised to {}.

Design notes

  • No Terraform change. The env vars were already on the functions.
  • Telemetry never breaks a request. Every recorder and the flush swallow their own failures; the flush is capped at 2s so a slow Grafana can't become a 30s lambda timeout.
  • No unbounded labels. Route patterns not paths, statement kinds not SQL, no ids or email addresses.
  • Delta temporality — cumulative counters from short-lived Lambda containers read as a reset storm to Mimir.
  • Off is a valid state. With no endpoint the OTel SDK is never required, so local dev and tests neither export nor pay the load cost. NODE_ENV=test also drops LOG_LEVEL to error so the access log doesn't drown jest output.
  • Preview stacks inherit the OTLP vars (preview-env.yml copies the live lambda env), so their telemetry arrives tagged pr-<N> — the same treatment their Sentry errors get.

Design doc: docs/superpowers/specs/2026-09-06-grafana-telemetry-design.md

Cost

OTel adds 245 KB minified per bundle (528 KB total, 136 KB zipped).

Testing

  • Shared packages green in CI's own dependency order: lambda-telemetry -> rbac -> types -> lambda-auth -> lambda-http — 28 / 30 / 30 / 52 tests. 13 of those are new tests covering the dispatch instrumentation, and 28 cover the telemetry package (including one that builds the real OTel providers, so an SDK API change fails loudly instead of silently dropping metrics).
  • All six lambdas typecheck and bundle.
  • Lambda unit suites: 81 auth, 61 projects, 80 expenditures, 41 users, 86 reports.
  • Not run: the e2e suites — they need the Postgres container, and Docker wasn't available in this environment.

🤖 Generated with Claude Code

nourshoreibah and others added 3 commits September 6, 2026 18:48
The lambdas have carried OTEL_EXPORTER_OTLP_ENDPOINT and
OTEL_EXPORTER_OTLP_HEADERS since 2dacc6d, but nothing in the repo read
them, so the Grafana account received nothing.

Adds @branch/lambda-telemetry, which exports OTLP metrics and structured
logs, and wires it into the two places every request already passes
through:

- dispatch() records latency, status, cold starts, auth refusals and
  unhandled errors for every route in all six services, emits one access
  log per request, and flushes before returning -- Lambda freezes the
  container on return, so an unflushed record is never seen.
- each db.ts passes `log: kyselyTelemetryLog`, measuring every statement
  and logging the slow and failed ones.

Controllers add domain metrics on top: login outcomes, invitations,
project and expenditure changes, donation counts and amounts, report
generation duration, size and outcome.

Logs go to stdout as JSON (CloudWatch is unchanged) and to Loki, carrying
the request id, route, status, duration and caller. serverError() routes
through the same logger, so every 500 is structured.

Constraints held throughout: telemetry never breaks a request (every
recorder swallows its own failures, the flush is capped at 2s), no
unbounded labels (route patterns not paths, statement kinds not SQL, no
ids), delta temporality because Lambda containers are short-lived, and
the SDK is never loaded at all when no endpoint is configured -- which is
what keeps local dev and tests offline and quiet.

No Terraform change: the env vars were already there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapse the explanatory blocks added by the telemetry work to one terse
line each, and drop the ones that only restate the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop the added comments that only restate the code, and collapse the last
three multi-line blocks. What survives is the non-obvious part: label
cardinality rules, the delta-temporality rationale, the tri-state provider
sentinel, and the OTLP header format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Sep 7, 2026
@nourshoreibah nourshoreibah added the no-review The PR review bot won't run label Sep 7, 2026
github-actions Bot added a commit that referenced this pull request Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-review The PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant