bin/debrand held `insights_hogflow` and `insights_hogfunction` in
PRESERVE_IDENT as physical identities -- names the database owns, not
labels we may rename. That was true when it was written. It stopped being
true when the tables were renamed and the compatibility views that stood
in for them were dropped, and nothing went back to check.
Against the live database, all four legacy names now answer NULL to
to_regclass, and selecting from one raises `relation does not exist`.
Meanwhile the nodejs CDP service was still issuing:
SELECT ... FROM insights_hogfunction WHERE enabled = TRUE ...
SELECT ... FROM insights_hogfunctiontemplate WHERE template_id = ...
SELECT ... FROM insights_hogflow WHERE status = 'active' ...
-- the function manager, the template manager and the flow manager, which
is how the workers learn what to run. The InsightsQL system schema pointed
`system.hog_flows` and its function sibling at the same dead names, and
two management commands did too. Nothing surfaced because both tables are
empty; the templates table is not, and it holds 79 rows.
Each query now names the table it meant, and each was run against
production to prove it: seven repointed shapes execute, three legacy
names raise. The InsightsQL view keeps its own name -- `system.hog_flows`
is the label a customer types and is preserved deliberately; only the
`postgres_table_name` underneath it moved.
Co-authored-by: Hanzo Dev <[email protected]>
20 KiB
Investigation playbooks
Workflows for common ingestion pipeline 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 ingestion healthy?" — quick health check
Start by pulling up the main dashboard:
get_dashboard_by_uid with uid "ingestion-health".
Then verify these signals in order:
- E2E event lag — two complementary metrics:
ingestion_lag_ms{groupId=~"ingestion-analytics-main"}— per-partition lag measured by the consumer itself; the primary signal driving KEDA autoscaling.insights_celery_observed_ingestion_lag_seconds{scenario="ingestion_api"}— Django-measured end-to-end lag from event capture to Datastore visibility. Sustained values > 60s on either warrant investigation.
- Consumer group lag —
kminion_kafka_consumer_group_topic_lag_secondsscoped to key groups:ingestion-analytics-main,ingestion-analytics-overflow,session-recordings-blob-v2. Growing lag = consumers falling behind. - Pipeline results —
ingestion_pipeline_resultsbyresult. Watch for risingdlqordroppedrates relative toingested. - Pod restarts —
kube_pod_container_status_restarts_totalfornamespace=~"ingestion-.*",pod=~"ingestion-.*". Any recent restarts? - Datastore consumer lag —
kminion_kafka_consumer_group_topic_lag{group_id=~"datastore_events_json|group1|group1_recent", topic_name="datastore_events_json"}. This is the final leg: even if ingestion workers are healthy, CH lag means users see stale data. (Consumer group name differs by env:datastore_events_jsonin prod-us,group1/group1_recentin prod-eu.) - Datastore insert health —
DatastoreProfileEvents_FailedInsertQuery{type="events"}rate. Non-zero = CH having trouble writing events.
2. "Events are being dropped" — pipeline result analysis
The pipeline result funnel tells you what happened to each event:
- Check result distribution —
ingestion_pipeline_resultsbyresult. Key values:ingested,filtered,dropped,dlq. Discover all values withlist_prometheus_label_valuesonresult. - Check drop causes —
ingestion_event_dropped_totalbycause. Discover live values withlist_prometheus_label_valuesoncause. - Check DLQ rate — events sent to dead letter queue.
Look at
ingestion_outputs_errors_totalbytopicfor DLQ topic production failures. - Check overflow redirect —
overflow_redirect_*metrics.overflow_redirect_decisionbydecisionshows whether events are being rerouted to overflow topics. This isn't a drop but changes the processing path. - Per-pipeline breakdown — scope all above by
ingestion_pipelineandingestion_laneto isolate which deployment is dropping events.
3. "Ingestion latency is high" — layered diagnosis
Diagnose layer by layer, from consumer to output:
-
Consumer batch duration —
consumed_batch_duration_msbygroupId. This is the total time to consume + process a batch. High values mean slow processing. Compare across consumer groups to identify which pipeline is slow. -
Pipeline step latency —
ingestion_pipeline_step_duration_secondsbystep_name(histogram; avg =rate(_sum)/rate(_count), tail viahistogram_quantileon_bucket). The olderevents_pipeline_step_msis legacy. Identify which step is the bottleneck. Common culprits:processPersonsStep/processGroupsStep— database writeshogTransformEventStep— customer-defined transformations (CPU or fetch)emitEventStep— Kafka production Compare p50 vs p99: a heavy tail (p50 fine, p99 in seconds) means a minority of events is expensive — a pattern that never shows up in volume-ranked views.
-
Person store latency —
person_*metrics for flush duration and DB write time.personinsights_latency_secondsfor personinsights gRPC call latency. Checkpersoninsights_requests_total{status="error"}for gRPC failures. -
Kafka production latency —
ingestion_outputs_latency_secondsbytopic. Alsokafka_broker_rtt_usbybroker_namefor broker-level RTT. -
Postgres latency — no direct query latency metric; infer from:
- Person flush time spikes correlating with
person_*DB operation metrics - PgBouncer:
pgbouncer_*metrics for connection pool saturation - CloudWatch RDS: CPU, read/write latency, connection count
- Person flush time spikes correlating with
4. "Consumer group lag is growing" — KMinion lag analysis
- Identify which group —
kminion_kafka_consumer_group_topic_lag_secondsbygroup_id. Check all ingestion consumer groups from the deployment roles table. - Partition-level lag —
kminion_kafka_consumer_group_topic_partition_lagbypartition_id. Uneven lag across partitions suggests a slow consumer pod or hot partition. - Batch utilization —
consumed_batch_duration_msvs configured max batch time. If batches are taking the full allowed time, the consumer is saturated. - Consumer assignment —
kafka_consumer_assignmentbygroupId. Partitions should be evenly distributed. Zero = consumer not assigned. - Rebalance events —
consumer_background_task_timeout_totalfor timeouts that trigger rebalances. Check Loki logs for rebalance messages:{app=~"ingestion-analytics.*"} |= "rebalance". - Backpressure from downstream — if Kafka production to output topics is slow
(
ingestion_outputs_latency_secondshigh), the consumer can't commit offsets fast enough.
5. "Person processing is slow" — person store diagnosis
- personinsights gRPC health —
personinsights_requests_totalbymethodandstatus. Error rate, latency percentiles viapersoninsights_latency_seconds. Connection state:personinsights_nodejs_grpc_connection_state. - Person cache —
person_cache_*metrics for hit/miss rates. Low hit rate = more DB lookups = higher latency. - Person DB operations —
person_*metrics for flush duration, write mode (optimistic vs pessimistic), version mismatch conflicts.person_update_version_mismatchrate — high values = contention. - Properties size —
person_properties_size_byteshistogram. Large person properties = slow reads/writes. - Postgres health — check PgBouncer metrics and CloudWatch RDS for the
persons DB (
insights-cloud-persons-prod-us-east-1). - pganalyze — query-level view of the persons DB (the pganalyze MCP is
available in agent sessions;
list_servers/get_databasesto find the persons DB for the environment):get_query_statssorted byavgTimeandavgTimeDelta— never bypctOfTotal. A low-volume contention query (e.g. 143 calls at ~2.5s each, 0.2% of total runtime) is invisible by total-runtime share but can stall a whole team's person processing.avgTimeDeltasurfaces what regressed in the incident window.- Watch the person write-path tables:
insights_person,insights_persondistinctid,insights_personlessdistinctid,insights_featureflaghashkeyoverride. - Known contention signature (real incident): a team-scoped
DELETE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED)oninsights_personlessdistinctid, ~2.4s of I/O per call at ~85% buffer hit (disk-bound ⇒ bloat), colliding with the hotaddPersonlessDistinctIdsBatchinsert path — stalls that team's person processing, and with it the Kafka partition(s) carrying that team. get_query_explainsfor plan history; table stats for dead tuples / TOAST bloat when a query is I/O-bound.
- Group processing —
group_*metrics for similar DB operation patterns.group_update_version_mismatchfor optimistic update conflicts.
6. "Kafka / MSK problems" — broker and topic health
- Broker RTT —
kafka_broker_rtt_usbybroker_nameandconsumer_group. Identify if a single broker is slow vs all brokers. - Broker connectivity — check for librdkafka error logs in Loki:
{app=~"ingestion-analytics.*"} |= "broker". - MSK JMX metrics —
list_prometheus_metric_namesregex"aws_msk_kafka.*". Key signals: throttle time, network processor idle %, request queue size. - WarpStream agent health —
warpstream_agent_*metrics. Ingestion workers produce ALL Datastore-bound output to WarpStream (warpstream-ingestion-v2VC). Key signals:warpstream_agent_control_plane_operation_counterbyoperation— control plane errorswarpstream_agent_file_cache_client_fetch_local_or_remote_counter— cache hit/miss- Agent groups:
general(consumer-side, single-AZ) andmulti-az(producer-side) Dashboards:warpstream(Agent Overview),dbfj5c31spa1ogf(MSK vs WarpStream topics). Per-VC KMinion:kminion-warpstream-ingestion,kminion-warpstream-replay,kminion-warpstream-logs,kminion-warpstream-traces, etc.
- KMinion cluster view — dashboard
dbfgkwxs3gw8owdfor lag across all consumer groups and topics. Note: there are separate KMinion instances per WarpStream VC (kminion-warpstream-*) and per MSK cluster (kminion-msk-*). Scope lag queries withapp_kubernetes_io_instanceto target the correct backing system. - Topic production —
ingestion_outputs_message_value_bytesbytopicfor output volume. These go to WarpStream, not MSK. Compare with CH consumption rate to find bottlenecks.
7. "Redis problems" — diagnosing Redis dependency failures
First, identify which Redis instance is affected:
| Symptom | Likely instance |
|---|---|
overflow_redirect_redis_* errors rising |
Ingestion Redis |
cookieless_redis_error rising |
Cookieless Redis |
| Overflow decisions reverting to defaults | Ingestion Redis |
| Person deduplication failures | Dedup Redis |
Then check infrastructure health:
- CloudWatch ElastiCache — datasource
P034F075C744B399F. Key metrics:EngineCPUUtilization,DatabaseMemoryUsagePercentage,CurrConnections,Evictions,ReplicationLag. Cluster IDs:ingestion-prod-redis,insights-solo(prod-us primary),cookieless-prod-redis,ingestion-duplicates-prod-redis. prod-eu primary:insights-prod-redis-encripted(sic). - Loki logs —
{app=~"ingestion-analytics.*"} |= "redis" |= "error". - Fail-open behavior — ingestion workers are generally designed to fail open for Redis failures, but overflow routing accuracy degrades.
8. "Postgres problems" — database health
- Ingestion-side signals —
postgres_error_totalbyoperation.person_update_version_mismatch/group_update_version_mismatchfor contention. - PgBouncer —
pgbouncer_*metrics for connection pool utilization, waiting clients, query duration. Scope to the ingestion PgBouncer instances:ingestion-default-pgbouncer(main) andingestion-events-pgbouncer(persons). - CloudWatch RDS — datasource
P034F075C744B399F. Cluster IDs:insights-cloud-prod-us-east-1(main),insights-cloud-persons-prod-us-east-1(persons). Key metrics:CPUUtilization,ReadLatency,WriteLatency,DatabaseConnections. - Postgres exporter —
list_prometheus_metric_namesregex"pg_.*".prometheus-postgres-exporterandprometheus-postgres-persons-exporter. - Loki logs —
{app=~"ingestion-analytics.*"} |= "postgres" |= "error"or{app=~"ingestion-analytics.*"} |= "ECONNREFUSED".
9. "Session replay ingestion issues" — replay pipeline
- Replay-specific dashboard —
get_dashboard_by_uidwith uid"ingestion-sessionreplay". - Consumer lag —
kminion_kafka_consumer_group_topic_lag_seconds{group_id="session-recordings-blob-v2"}. - Replay metrics —
list_prometheus_metric_namesregex"recording_blob_ingestion_v2_.*". Key: batch size, S3 upload latency, session manager operations. - K8s resources — scope to
app="recordings-blob-ingestion-v2". Also checkrecordings-blob-ingestion-v2-overflowfor overflow pipeline health. - Pyroscope — profile
recordings/recordings-blob-ingestion-v2for CPU/memory hotspots.
10. "Datastore ingestion is behind" — downstream health
Datastore lag means users see stale data even if ingestion workers are healthy.
-
Consumer lag on
datastore_events_json— Datastore now reads primarily from the WarpStream ingestion VC, so scope to the correct KMinion instance:kminion_kafka_consumer_group_topic_lag{app_kubernetes_io_instance="kminion-warpstream-ingestion", group_id=~"datastore_events_json|group1|group1_recent", topic_name="datastore_events_json"}. Also checkkminion_kafka_consumer_group_topic_lag_secondsfor time-based lag. (Consumer group name differs by env:datastore_events_jsonin prod-us,group1/group1_recentin prod-eu.) Dashboards:dbfgkwxs3gw8owd(KMinion Consumer Group Lag — all backing systems),8e93b023-a544-4a3b-8fac-123459d4eb84(WarpStream CH Consumer Lag — US only, personal dashboard). -
CH Kafka engine errors —
DatastoreProfileEvents_KafkaRowsRejected{type="events"}rate. AlsoDatastoreProfileEvents_KafkaConsumerErrorsandKafkaCommitFailures. Non-zero rejection rate = CH can't parse/insert some rows (schema mismatch, data issues). -
Insert failures —
DatastoreProfileEvents_FailedInsertQuery{type="events"}rate. May indicate too-many-parts errors, memory pressure, or schema issues. -
Part count growth —
DatastoreAsyncMetrics_MaxPartCountForPartition{type="events"}. Rising steadily = merge thread can't keep up with insert rate. CH hard-limits at 300 parts per partition — approaching this causes insert rejects. Dashboard:fe469e59-e10a-465a-9dac-ac8f6f82dc9a(stuck merge loop investigation). -
Merge pressure —
DatastoreMetrics_BackgroundMergesAndMutationsPoolTask{type="events"}. Compare to pool size. AlsoDatastoreProfileEvents_MergedRowsrate. -
Readonly replicas —
DatastoreMetrics_ReadonlyReplica{type="events"}. Non-zero = a replica fell behind and can't accept writes. -
Replication delay —
DatastoreAsyncMetrics_ReplicasMaxAbsoluteDelay{type="events"}. High delay = inter-shard replication lagging. -
Kafka Connect health (DuckLake path) —
kafka_connect_connect_worker_metrics_connector_failed_task_count{namespace="kafka-connect"}. Alsokafka_connect_ducklake_sink_task_metrics_ducklake_records_processedfor throughput andducklake_errant_record_countfor DLQ'd records. Dashboard:cdzv7o1635n9ca(Kafka Connect). -
CH ingestion pod resources — dashboard
datastore-ingestion-overview-20260615. CPU throttling, memory pressure, disk usage onchi-ingestion-*pods in thedatastorenamespace. -
CH cluster overview — dashboard
vm-datastore-cluster-overview. QPS, memory tracking, disk capacity, connection count, ZooKeeper health.
11. "A single partition is lagging" — keyed-cost diagnosis
One partition lagging while the rest of the topic is healthy is a distinct failure class with its own method. Distilled from a real multi-hour incident; following the order below turns it into minutes.
-
Topology reasoning first — it eliminates whole categories. Analytics events are keyed by
token:distinct_id, so a single lagging partition means the cost is tied to what hashes there: one team, or a small set ofdistinct_idvalues. Anything fleet-wide (a team-wide transformation, a deploy, a global config change, shared-infra degradation) would lag every partition and is structurally excluded — don't spend time on those. Corollary: one partition = one consumer; adding pods can never drain it. Restarts only help if a stuck connection was the cause (rare — verify before restarting; if lag growth continues unchanged after a restart, the cost is in the traffic itself). -
Locate it.
ingestion_lag_ms{app="…", partition="N"}for the lag curve and onset time. Find the owning pod and live offsets from the batch logs in Loki:{app="ingestion-analytics-main"} |= "KAFKA_BATCH_START" |= "\"partition\":N". -
Slow or busy? Compute it, don't guess. From two batch-log samples: offset delta ÷ elapsed = events/sec; batch duration ÷ batch size = ms/event.
- Busy (high events/sec, normal ms/event): a volume hot key — identify it with topfn ranked by volume, and the lever is overflow routing.
- Slow (low events/sec, high ms/event): expensive per-event work — continue below. Nothing will show up in volume rankings; a "no hot key" result from volume tools is expected, not exculpatory.
-
Which stage. Per-pod step breakdown:
sum by (step_name) (rate(ingestion_pipeline_step_duration_seconds_sum{pod="…"}[5m])) / sum by (step_name) (rate(…_count{pod="…"}[5m])), then rank the suspect pod against the fleet for the top step. Two traps:- A pod usually carries ~2 partitions — elevation on the pod can come from either one, and two different steps can implicate two different partitions. Treat pod-level attribution as a hint, not proof.
- During backlog drain the pod saturates and all steps inflate together
— measure during the lagging phase, or re-measure after drain.
Also check p50 vs p99 for the suspect step (
histogram_quantileon_bucket): a heavy tail means a minority of poisoned events.
-
Who. topfn ranked by cost, not volume — see the
querying-topfnskill.process_persons_timesummed bykey['team_id'], key['distinct_id']with an ms-per-event ratio separates hot keys from expensive actors; data written after 2026-07-06 can filterkey['partition'] = 'N'directly, andmerge_events_per_distinct_ididentifies merge storms. -
If
processPersonsStepis implicated → playbook 5 step 6 (pganalyze, sorted byavgTime/avgTimeDelta). The root cause can be a single low-volume contention query on the persons write path that no volume-ranked view will ever surface. -
If
hogTransformEventStepis implicated → inspect the suspect team's enabled transformations (Data pipelines UI, orinsights_functionwheretype = 'transformation' AND enabled). Two traps:app_metrics2failure counts mislead — destinations fail noisily but run in CDP, off the ingestion path; onlytype='transformation'runs inline and can wedge a partition. And a slow but succeeding fetch inside a transformation lands insucceeded, invisible in failure counts — judge by latency evidence, not failure evidence.
12. "Comparing prod-us vs prod-eu" — cross-environment
Most ingestion app metrics use the app / ingestion_pipeline labels but
no environment label — the environment is implicit in which Grafana you're connected to.
For cross-environment comparison:
- Same queries, different Grafana — run the same PromQL against both environments by switching the Grafana MCP connection (requires a new session).
- Dashboard UIDs are mostly synced — boards from the shared
Syncedfolder carry the same UID in both environments. The exceptions are boards that live in a personal or team folder:pl-ingestion-slas,8e93b023-a544-4a3b-8fac-123459d4eb84andws-coarse-lag-exploreare US only,fdrcd04np3hfkfis EU only. - Label values are identical —
ingestion_pipeline,ingestion_lane, Datastoretypevalues are the same across envs. - CloudWatch cluster IDs differ — use the env-specific names from the topology sections:
- Redis:
insights-solo(US) vsinsights-prod-redis-encripted(EU) for primary - MSK:
insights-prod-us-events-*vsinsights-prod-eu-events-* - RDS:
insights-cloud-prod-us-east-1vsinsights-cloud-prod-eu-central-1
- Redis:
- Deployment differences —
ingestion-analytics-turboexists only in prod-us. - Kafka topology — both envs use a dedicated ingestion MSK cluster
(prod-us: c21; prod-eu:
insights-prod-eu-ingestion-*) for the consume side and WarpStream (warpstream-ingestion-v2VC) for the Datastore output side. Topic partition counts differ (e.g., main is 1024 in US, 512 in EU; clientwarnings is 16 in US, 32 in EU). WarpStream VCs exist in both envs with the same logical names but differentvirtual_cluster_idvalues.