The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
9.7 KiB
Investigation playbooks
Workflows for common capture service investigations. Each playbook describes what to check and in what order — construct PromQL live using the discovery workflow from the main skill doc.
1. "Is capture healthy?" — quick health check
Start by pulling up the main dashboard:
get_dashboard_by_uid with uid "capture".
Then verify these signals in order:
- HTTP error rate —
http_requests_duration_seconds_countbystatus, scoped bynamespace. Any sustained 5xx spike is the top-priority signal. - Kafka connectivity —
capture_kafka_any_brokers_down. Value of 1 on any pod is critical. - Event acceptance ratio —
rate(capture_events_ingested_total)/rate(capture_events_received_total). Sustained drop below ~0.95 warrants investigation. - Envoy backend health —
envoy_cluster_membership_healthyvsenvoy_cluster_membership_totalforenvoy_cluster_name=~"insights_capture-.*". Ratio < 1.0 outside of deploys is a problem. - Event restrictions staleness —
capture_event_restrictions_stalegauge. Value of 1 means restrictions aren't being refreshed from Redis. - Billing limits loaded —
capture_billing_limits_loaded_tokensbycache_key. Zero for any cache key means quota limits aren't loading.
2. "Events are being lost" — event lifecycle funnel
The event funnel: received -> ingested -> dropped/rerouted.
- Compare rates —
rate(capture_events_received_total)vsrate(capture_events_ingested_total). The gap is drops + reroutes. - Discover drop causes —
capture_events_dropped_totalbycause. Discover live values withlist_prometheus_label_valuesoncause. Key causes to watch:retryable_sink— Kafka producer failures (check Kafka health next)events_over_quota/recordings_over_quota/*_over_quota— billing limits hittoken_dropper— token-level restrictionkafka_message_size— individual event too large for Kafkano_distinct_id/no_event_name— malformed events from SDKs
- Check Kafka delivery —
capture_kafka_produce_errors_totalrate. Non-zero = messages failing to produce. - Check quota limits —
capture_quota_limit_exceededbyresource. - Check event restrictions —
capture_event_restrictions_appliedbyrestriction_type.drop_eventdirectly causes loss;force_overflowreroutes to overflow topic. - Check rerouting —
capture_events_rerouted_overflowbyreason. Events rerouted aren't lost but take a different path through the pipeline.
3. "Latency is high" — layered diagnosis
Diagnose layer by layer, outside-in:
-
Envoy layer —
envoy_cluster_upstream_rq_time_buckethistogram forenvoy_cluster_name=~"insights_capture-.*". Compute p99 withhistogram_quantile(0.99, ...). If Envoy latency is high but HTTP handler latency is normal, the issue is between Envoy and the pod (networking, connection pool). -
HTTP handler —
http_requests_duration_seconds_bucketscoped bynamespace. This is the end-to-end request latency inside the capture binary. Compare p99 across namespaces to isolate which pipeline is slow. -
Kafka produce RTT —
capture_kafka_produce_rtt_latency_usbybrokerandquantile. p99 > 100ms sustained = broker overload or network issues. p99 > 500ms = critical. Cross-reference with queue depth for backpressure signal. -
Redis latency — no direct latency metric in capture. Check
capture_event_restrictions_redis_fetch{result="error"}rate as a proxy. For actual Redis latency, use CloudWatch ElastiCache metrics via datasourceP034F075C744B399F. -
Kafka producer queue depth —
capture_kafka_producer_queue_depth/capture_kafka_producer_queue_depth_limit. Ratio > 0.8 = producer near saturation, will backpressure the HTTP handler.
4. "Kafka backpressure" — producer saturation
- Queue saturation —
capture_kafka_producer_queue_depth/capture_kafka_producer_queue_depth_limit. Also check bytes:capture_kafka_producer_queue_bytes/_bytes_limit. - Broker RTT —
capture_kafka_produce_rtt_latency_us{quantile="p99"}bybroker. Identify if a single broker is slow vs all brokers. - Broker connectivity —
capture_kafka_broker_connectedbybroker. Any broker at 0 across all pods needs immediate investigation. - Broker errors —
capture_kafka_broker_tx_errors_totaland_rx_errors_totalby broker.capture_kafka_broker_request_timeoutsby broker. - MSK throttle time — discover
aws_msk_*metrics withlist_prometheus_metric_namesregex"aws_msk_kafka.*throttle.*". Throttling means Kafka quota enforcement is active. - MSK network processor — look for idle % metric. < 30% idle = broker network thread saturation warning.
- Topic-level —
capture_kafka_produce_avg_batch_size_bytesand_eventsbytopic. Identify which topic is receiving the most load. - Downstream impact —
overflow_redirect_*metrics on the Node.js ingestion pipeline. These are NOT capture metrics but show how overflow topics are being consumed.
5. "Rate limiting / quota impact" — which limiters are active
Multiple layers of rate limiting can affect capture. Check in this order:
-
Billing quota (per-team, Redis-backed)
capture_quota_limit_exceededbyresource— which quota types are being hitcapture_billing_limits_loaded_tokensbycache_key— how many tokens are flagged- Drop causes:
events_over_quota,recordings_over_quota,exceptions_over_quota,llm_events_over_quota,survey_responses_over_quotaoncapture_events_dropped_total
-
Event restrictions (per-team, Redis-backed)
capture_event_restrictions_appliedbyrestriction_typedrop_event= hard drop,force_overflow= reroute to overflow topic,redirect_to_topic= reroute to custom topic,skip_person_processing= pass through but skip person
-
Global rate limiter (per-token+distinct_id, Redis-backed)
global_rate_limiter_*metrics for direct limiter decisions.capture_events_rerouted_overflow{reason="rate_limited"}rate — proxy signal from the overflow path.- Config:
GLOBAL_RATE_LIMIT_ENABLED,GLOBAL_RATE_LIMIT_TOKEN_DISTINCTID_THRESHOLD
-
Overflow routing (the combined effect)
capture_events_rerouted_overflowbyreason:rate_limited(GRL),force_limited(event restrictions),event_restriction(explicit overflow)
-
Contour rate limit (per-IP, Envoy-layer, NOT capture binary)
ratelimit_service_rate_limit_over_limit— actively limited IPsratelimit_service_rate_limit_total_hits— total evaluations- Labels:
domain="insights",key1,key2for rule identification - This Redis (
ratelimit-{env}-redis) is separate from capture's Redis instances
6. "Redis problems" — diagnosing Redis dependency failures
First, identify which Redis instance is affected:
| Symptom | Likely instance |
|---|---|
capture_event_restrictions_redis_fetch{result="error"} rising |
Primary or event restrictions Redis |
capture_event_restrictions_stale = 1 |
Primary or event restrictions Redis |
capture_billing_limits_loaded_tokens = 0 for a cache_key |
Primary Redis |
capture_events_rerouted_overflow{reason="rate_limited"} anomaly |
GRL Redis |
ratelimit_service_* errors |
Contour rate limit Redis (not capture) |
Then check infrastructure health:
-
CloudWatch ElastiCache — use datasource
P034F075C744B399F. Key metrics:EngineCPUUtilization,DatabaseMemoryUsagePercentage,CurrConnections,Evictions,ReplicationLag. Cluster IDs:- Primary:
insights-solo(prod-us) orinsights-prod-redis-encripted(prod-eu; sic — typo in actual name) - GRL:
capture-globalratelimit-prod-redis - Rate limit:
ratelimit-prod-redis
- Primary:
-
prometheus-redis-exporter — search VictoriaMetrics for Redis exporter metrics if available:
list_prometheus_metric_namesregex"redis_.*". -
Fail-open behavior — capture is designed to fail open for most Redis failures:
- Quota limiter: if Redis is down, limits aren't refreshed but cached limits still apply
- Event restrictions:
event_restrictions_fail_open_after_secsconfig controls how long after a Redis failure before restrictions are bypassed - GRL: if Redis is down, rate limiting stops (traffic passes through)
7. "Comparing prod-us vs prod-eu" — cross-environment
Most capture app metrics use namespace/container labels but no environment label —
the environment is implicit in which Grafana instance you're connected to.
For cross-environment comparison:
- MSK metrics use
environment="prod-us"or"prod-eu"label — these can be compared in a single query. - CloudWatch datasource has access to both regions — use region selector in the query.
- Dashboard variables — some dashboards have an
environmentvariable. Check withget_dashboard_by_uidand look at template variables. - Kafka topic names differ by partition count —
ingestion-analytics-main-1024(US) vsingestion-analytics-main-512(EU). Thetopiclabel works for comparing produce patterns but values won't match across envs. - Both envs have a dedicated ingestion MSK cluster — capture-analytics and capture-ai
produce to
msk-ingestion, not the events cluster (prod-us: c21; prod-eu:insights-prod-eu-ingestion-*).