feat(observability): send metrics and logs to Grafana Cloud - #399
Open
nourshoreibah wants to merge 4 commits into
Open
feat(observability): send metrics and logs to Grafana Cloud#399nourshoreibah wants to merge 4 commits into
nourshoreibah wants to merge 4 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The lambdas have carried
OTEL_EXPORTER_OTLP_ENDPOINTandOTEL_EXPORTER_OTLP_HEADERSsince2dacc6d, but nothing in the repo ever read them — so the Grafana Cloud account has been receiving nothing. Observability today is Sentry (uncaught throws) plus unstructuredconsole.*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:http.server.requestshttp.server.request.durationfaas.cold_startshttp.server.auth_failuresunauthenticated|forbiddenhttp.server.unhandled_errorsdb.client.operation.durationThe last one comes from
log: kyselyTelemetryLogon 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 signalbranch.auth.registrations—invitation_requiredshows people bouncing off the invite gatebranch.users.invitedbranch.projects.changed— deletes cascade into expenditures, reports and S3branch.expenditures.changed/.amount— the approval funnel and spend by categorybranch.donations.recorded/.amountbranch.reports.generated/.generation.duration/.size— the slowest path in the backend, nearest the 30s timeoutLogs 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; anErroris flattened rather than serialised to{}.Design notes
required, so local dev and tests neither export nor pay the load cost.NODE_ENV=testalso dropsLOG_LEVELtoerrorso the access log doesn't drown jest output.preview-env.ymlcopies the live lambda env), so their telemetry arrives taggedpr-<N>— the same treatment their Sentry errors get.Design doc:
docs/superpowers/specs/2026-09-06-grafana-telemetry-design.mdCost
OTel adds 245 KB minified per bundle (528 KB total, 136 KB zipped).
Testing
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).🤖 Generated with Claude Code