@forgecart/flight-observability 0.202608212205.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ # pg-custom-queries.yaml v1 — the shared PG custom-metric contract (Flight
2
+ # Deck P0 #967 → P4 #973). ONE dialect, two consumers: locally the shop-obs
3
+ # postgres_exporter ×3 (`--extend.query-path`), in prod CNPG's
4
+ # `monitoring.customQueriesConfigMap` renders THIS SAME file (architecture §7
5
+ # item 3) — the metric names below are contract, never fork them per env.
6
+ #
7
+ # v1 scope (§3 rows 6): idle-in-transaction age/count (tech-debt #10's
8
+ # left half — the FcIdleInTxAge alert consumes fc:pg:idle_in_tx_age_max_seconds)
9
+ # and coordinator↔worker backend attribution. The P1c extensions (in-flight
10
+ # age percentiles, connection ages, fan-out counts) append here — additive.
11
+
12
+ pg_idle_in_tx:
13
+ query: >-
14
+ SELECT
15
+ COALESCE(EXTRACT(EPOCH FROM
16
+ max(now() - xact_start) FILTER (WHERE state = 'idle in transaction')), 0)::float AS age_max_seconds,
17
+ count(*) FILTER (WHERE state = 'idle in transaction')::float AS count
18
+ FROM pg_stat_activity
19
+ WHERE backend_type = 'client backend'
20
+ metrics:
21
+ - age_max_seconds:
22
+ usage: GAUGE
23
+ description: Age of the oldest idle-in-transaction backend (seconds)
24
+ - count:
25
+ usage: GAUGE
26
+ description: Number of idle-in-transaction backends
27
+
28
+ pg_citus_backends:
29
+ query: >-
30
+ SELECT
31
+ CASE WHEN application_name LIKE 'citus%%' THEN 'citus' ELSE 'client' END AS source,
32
+ count(*)::float AS count
33
+ FROM pg_stat_activity
34
+ WHERE backend_type = 'client backend'
35
+ GROUP BY 1
36
+ metrics:
37
+ - source:
38
+ usage: LABEL
39
+ description: Backend origin (citus = coordinator fan-out, client = direct)
40
+ - count:
41
+ usage: GAUGE
42
+ description: Client backends by origin
43
+
44
+ # Outbox staleness (P4 #973 — FcOutboxOldest's source). Oldest UNSENT row that
45
+ # is actually DUE (deliver_at in the future is a scheduled send, not a stall).
46
+ pg_outbox:
47
+ # Column names are TypeORM camelCase and MUST be quoted — the unquoted
48
+ # snake_case draft failed live on the Q12 verification pass
49
+ # (`pg_outbox pq: column "created_at" does not exist`, exporter served
50
+ # nothing and FcOutboxOldest was input-starved).
51
+ query: >-
52
+ SELECT
53
+ count(*) FILTER (WHERE NOT sent)::float AS backlog,
54
+ COALESCE(EXTRACT(EPOCH FROM (now() - min("createdAt") FILTER (
55
+ WHERE NOT sent AND ("deliverAt" IS NULL OR "deliverAt" <= now())
56
+ ))), 0)::float AS oldest_unsent_seconds
57
+ FROM queue_outbox
58
+ metrics:
59
+ - backlog:
60
+ usage: GAUGE
61
+ description: Unsent outbox rows
62
+ - oldest_unsent_seconds:
63
+ usage: GAUGE
64
+ description: Age of the oldest due-but-unsent outbox row (seconds)
@@ -0,0 +1,188 @@
1
+ # fc-alerts.yml — the ONE alert-rules file both worlds evaluate (architecture
2
+ # §7 item 5): locally the shop-obs vmalert fires into the BLACKHOLE notifier,
3
+ # prod (P4 #973) routes the identical file to Discord via alertmanager. Never
4
+ # fork per env — env-specific thresholds select on the `env` external label.
5
+ #
6
+ # P1c seeded the cardinality self-watch; P4 lands the full set. Thresholds
7
+ # marked [Q9-PROPOSED] ratify with the operator at final acceptance (E3) —
8
+ # they alert from day one so the one-week forgecart-test dry-run (E2) can
9
+ # judge them against reality. `CitusBackendsHigh` already exists in
10
+ # forgecart/cloud — displayed on Row 4, deliberately NOT duplicated here.
11
+ groups:
12
+ - name: fc-alerts-self
13
+ rules:
14
+ # C5 — the series budget is a hard ceiling per environment: 60k local,
15
+ # 150k prod. Breach = a cardinality leak (per-run/per-test label escape)
16
+ # until proven otherwise; the offender census starts at
17
+ # /api/v1/status/tsdb on the breaching store. F1's ~1k-workspace-pod
18
+ # trigger (tech-debt #1) surfaces HERE first by design.
19
+ - alert: FcCardinalityBudget
20
+ expr: >-
21
+ fc:self:active_series{env="local"} > 60000
22
+ or
23
+ fc:self:active_series{env!="local"} > 150000
24
+ for: 10m
25
+ labels:
26
+ severity: warning
27
+ annotations:
28
+ summary: 'TSDB active series over the {{ $labels.env }} budget'
29
+ description: 'fc:self:active_series = {{ $value }} — cardinality leak until proven otherwise (architecture §2.2/C5).'
30
+
31
+ # ── tech-debt #10, closed verbatim (P4 #973 item 5) ─────────────────────────
32
+ - name: fc-alerts-held-tx
33
+ rules:
34
+ - alert: FcHeldTxPoolWait
35
+ expr: fc:convoy:heldtx == 1
36
+ for: 2m
37
+ labels:
38
+ severity: warning
39
+ annotations:
40
+ summary: 'Held-tx convoy sustained 2m'
41
+ description: 'Waiters queue on the held-transaction lane while it sits pinned — the pool-convoy class is invisible to CPU/latency dashboards by construction (tech-debt #10).'
42
+ - alert: FcHeldTxAcquireWaitCritical
43
+ expr: fc:pool:acquire_wait_p99_seconds{lane="held-tx"} > 5
44
+ for: 2m
45
+ labels:
46
+ severity: critical
47
+ annotations:
48
+ summary: 'Held-tx acquire wait p99 over 5s'
49
+ description: 'p99 = {{ $value }}s — editing transactions are starving; check fc:pgbouncer:pinned_share and the coordinator connection budget (raise-together rule).'
50
+ - alert: FcIdleInTxAge
51
+ expr: fc:pg:idle_in_tx_age_max_seconds > 120
52
+ for: 5m
53
+ labels:
54
+ severity: warning
55
+ annotations:
56
+ summary: 'Idle-in-transaction backend older than 120s'
57
+ description: 'Oldest idle-in-tx = {{ $value }}s — a held-open transaction pins a real coordinator connection through the pooler (tech-debt #10).'
58
+
59
+ - name: fc-alerts-pooler
60
+ rules:
61
+ - alert: FcPoolerMaxwait
62
+ expr: fc:pgbouncer:maxwait_seconds > 2
63
+ for: 2m
64
+ labels:
65
+ severity: warning
66
+ annotations:
67
+ summary: 'PgBouncer pool maxwait over 2s'
68
+ description: 'db_family={{ $labels.db_family }} maxwait = {{ $value }}s — clients queue at the pooler; the F5 pinned-share panel names the culprit side.'
69
+ # F5's invariant as an alert: the held-lane share of standing server
70
+ # connections. [Q9-PROPOSED] 0.8.
71
+ - alert: FcPinnedShareHigh
72
+ expr: fc:pgbouncer:pinned_share > 0.8
73
+ for: 5m
74
+ labels:
75
+ severity: warning
76
+ annotations:
77
+ summary: 'Held-tx lane consumes >80% of standing pooler servers'
78
+ description: 'pinned_share = {{ $value }} — client-paced transactions are crowding out multiplexed traffic (§3.4).'
79
+
80
+ - name: fc-alerts-leak
81
+ rules:
82
+ - alert: FcCloseWaitLeak
83
+ expr: fc:leak:close_wait > 50
84
+ for: 15m
85
+ labels:
86
+ severity: warning
87
+ annotations:
88
+ summary: 'CLOSE_WAIT sockets accumulating on {{ $labels.actor }}'
89
+ description: '{{ $value }} CLOSE_WAIT sockets held 15m+ — the peer closed, the app never reaped (Row 8; application_name from #969 names the PG-side twin).'
90
+ # ~1 MB/min sustained for an hour. [Q9-PROPOSED]
91
+ - alert: FcRssLeak
92
+ expr: fc:leak:rss_slope_bytes_per_s > 17476
93
+ for: 60m
94
+ labels:
95
+ severity: warning
96
+ annotations:
97
+ summary: 'RSS climbing ~1MB/min for 1h on {{ $labels.actor }}'
98
+ description: '30m slope = {{ $value }} B/s sustained — leak candidate; replay the window and read the actor heat bands.'
99
+
100
+ - name: fc-alerts-latency
101
+ rules:
102
+ # [Q9-PROPOSED] 200ms.
103
+ - alert: FcEventLoopLag
104
+ expr: fc:loop:lag_p99_seconds > 0.2
105
+ for: 5m
106
+ labels:
107
+ severity: warning
108
+ annotations:
109
+ summary: 'Event-loop lag p99 over 200ms on {{ $labels.service }}'
110
+ description: 'p99 = {{ $value }}s — the serve tier is starving; correlate with fc:actor:psi_cpu_some_ratio and the melee lanes.'
111
+ # [Q9-PROPOSED] tx p99 above 80% of the live budget gauge (F9).
112
+ - alert: FcTxNearBudget
113
+ expr: fc:tx:p99_seconds > 0.8 * fc:tx:budget_seconds
114
+ for: 10m
115
+ labels:
116
+ severity: warning
117
+ annotations:
118
+ summary: 'Transaction p99 above 80% of TRANSACTION_TIME_BUDGET_MS'
119
+ description: 'p99 = {{ $value }}s vs the budget gauge — client-paced spans are drifting toward the kill window.'
120
+
121
+ - name: fc-alerts-probes
122
+ rules:
123
+ - alert: FcDnsProbeFailing
124
+ expr: fc:dns:probe_success == 0
125
+ for: 5m
126
+ labels:
127
+ severity: warning
128
+ annotations:
129
+ summary: 'DNS probe {{ $labels.probe }} failing 5m'
130
+ description: 'system-fail + control-ok is the router rebind-protection signature; both-fail is an egress/DNS outage (§3 row 17).'
131
+ # [Q9-PROPOSED] 120s.
132
+ - alert: FcOutboxOldest
133
+ expr: fc:outbox:oldest_seconds > 120
134
+ for: 5m
135
+ labels:
136
+ severity: warning
137
+ annotations:
138
+ summary: 'Oldest due-but-unsent outbox row over 120s'
139
+ description: 'oldest = {{ $value }}s, backlog = {{ with query "fc:outbox:backlog" }}{{ . | first | value }}{{ end }} — the relay is stalled or the broker is refusing publishes.'
140
+
141
+ - name: fc-alerts-doctrine
142
+ rules:
143
+ # G5's tripwire in alert form: weights never quotas — nonzero throttling
144
+ # means someone added a CPU limit (a namespace LimitRange injecting
145
+ # defaults is the usual prod offender, §6.8).
146
+ - alert: FcThrottleNonZero
147
+ expr: fc:actor:throttled_periods_delta > 0
148
+ for: 1m
149
+ labels:
150
+ severity: critical
151
+ annotations:
152
+ summary: 'CPU throttling on {{ $labels.actor }} — a quota exists'
153
+ description: 'nr_throttled delta = {{ $value }} — the limits doctrine is weights NEVER quotas (§6.1/G5); find and remove the quota, never tune it.'
154
+
155
+ - name: fc-alerts-convoys
156
+ rules:
157
+ - alert: FcConvoyPgbouncer
158
+ expr: fc:convoy:pgbouncer == 1
159
+ for: 5m
160
+ labels:
161
+ severity: warning
162
+ annotations:
163
+ summary: 'Pooler convoy: clients waiting while servers sit active'
164
+ description: 'The multiplexing layer is the bottleneck — read the Multiplexing page top-down (§4.2).'
165
+ - alert: FcConvoyCitusFanout
166
+ expr: fc:convoy:citus_fanout == 1
167
+ for: 5m
168
+ labels:
169
+ severity: warning
170
+ annotations:
171
+ summary: 'Citus fan-out convoy: coordinator backends saturated by distributed queries'
172
+ description: 'Distributed-query fan-out is crowding the coordinator (>256 citus-source backends sustained).'
173
+ - alert: FcConvoyGrpcParked
174
+ expr: fc:convoy:grpc_parked == 1
175
+ for: 2m
176
+ labels:
177
+ severity: warning
178
+ annotations:
179
+ summary: 'gRPC channel parked with in-flight RPCs ({{ $labels.service }}→{{ $labels.target }})'
180
+ description: 'RPCs issued against a channel that is not READY — the park signature from the dual7/dual12 forensics (#969).'
181
+ - alert: FcConvoyPrefetchHol
182
+ expr: fc:convoy:prefetch_hol == 1
183
+ for: 5m
184
+ labels:
185
+ severity: warning
186
+ annotations:
187
+ summary: 'MQ prefetch head-of-line: unacked pinned at consumers × effective prefetch'
188
+ description: 'Consumers hold their full prefetch window unacked (F4 divisor) — slow-consumer HOL, not backlog.'
@@ -0,0 +1,409 @@
1
+ # fc-recording.yml v1 — THE `fc:` recording-rule contract (Flight Deck P0,
2
+ # launch#46 / #967; architecture §2.1/§3). This ONE file is the local↔prod
3
+ # parity seam: dashboards query `fc:*` ONLY, and every exporter-label
4
+ # difference between the two worlds is absorbed HERE. The same file is
5
+ # evaluated by the local shop-obs vmalert (blackhole notifier) and by the prod
6
+ # victoria-metrics-k8s-stack vmalert (P4 #973) — never fork it per env.
7
+ #
8
+ # Cardinality doctrine (§2.2): every label on a rule below comes from a BOUNDED
9
+ # set (actor, db_family, pg_node, state, service, probe target). No per-test,
10
+ # per-channel, per-run, or per-op-name label may ever appear in this file.
11
+ #
12
+ # v1 rows (P0): host weather, per-actor CPU/RSS/PSI/throttle, PG backends by
13
+ # db_family + idle-in-tx, MQ transport basics, sockets, DNS, victim canary,
14
+ # redis/es/ch health, TSDB self-watch. The gRPC row is deliberately ABSENT
15
+ # until #969 lands the generic package/grpc seam (F3 honesty rule — its only
16
+ # P0 truth is Plane-B rpc.* markers); `fc:workspace:*` (F1 bounded families)
17
+ # arrives with kube-state-metrics in prod (P4) — local per-pod truth lives in
18
+ # Plane B / replay.
19
+ groups:
20
+ - name: fc-host
21
+ interval: 15s
22
+ rules:
23
+ - record: fc:host:cpu_busy_ratio
24
+ expr: 1 - avg(rate(node_cpu_seconds_total{mode="idle"}[1m]))
25
+ - record: fc:host:cpu_steal_ratio
26
+ expr: avg(rate(node_cpu_seconds_total{mode="steal"}[1m]))
27
+ - record: fc:host:psi_cpu_some_ratio
28
+ expr: rate(node_pressure_cpu_waiting_seconds_total[1m])
29
+ - record: fc:host:psi_mem_some_ratio
30
+ expr: rate(node_pressure_memory_waiting_seconds_total[1m])
31
+ - record: fc:host:psi_io_some_ratio
32
+ expr: rate(node_pressure_io_waiting_seconds_total[1m])
33
+ - record: fc:host:load1
34
+ expr: node_load1
35
+ - record: fc:host:mem_available_ratio
36
+ expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
37
+ # The red-weather stamp (§3 row 3): swap-out while memory looks "fine"
38
+ # is the melee-killing signature steal alone cannot explain.
39
+ - record: fc:host:pswpout_rate
40
+ expr: rate(node_vmstat_pswpout[1m])
41
+
42
+ - name: fc-actor
43
+ interval: 15s
44
+ rules:
45
+ # Actor identity is BOUNDED. Docker containers are discovered via the
46
+ # system containerd (moby namespace — this host's docker runs the
47
+ # containerd-snapshotter layout, and docker propagates NO compose labels
48
+ # to containerd), so the stable bounded key is the IMAGE basename
49
+ # (postgresql-citus, rabbitmq-delayed, redis, k3s, …) — container-hash
50
+ # `name`s recycle on every recreate and must never become actors.
51
+ # `fc-*` systemd scopes (P2 governance, raw whitelist) map from `id`.
52
+ - record: fc:actor:cpu_ratio
53
+ expr: >-
54
+ sum by (actor) (
55
+ label_replace(rate(container_cpu_usage_seconds_total{image=~".+"}[1m]), "actor", "$1", "image", ".*/([^/:]+):[^/]+")
56
+ or
57
+ label_replace(rate(container_cpu_usage_seconds_total{image="", id=~".*/fc-[^/]+\\.(scope|slice)"}[1m]), "actor", "$1", "id", ".*/(fc-[^/]+)\\.(?:scope|slice)")
58
+ )
59
+ - record: fc:actor:rss_bytes
60
+ expr: >-
61
+ sum by (actor) (
62
+ label_replace(container_memory_rss{image=~".+"}, "actor", "$1", "image", ".*/([^/:]+):[^/]+")
63
+ or
64
+ label_replace(container_memory_rss{image="", id=~".*/fc-[^/]+\\.(scope|slice)"}, "actor", "$1", "id", ".*/(fc-[^/]+)\\.(?:scope|slice)")
65
+ )
66
+ - record: fc:actor:psi_cpu_some_ratio
67
+ expr: >-
68
+ sum by (actor) (
69
+ label_replace(rate(container_pressure_cpu_waiting_seconds_total{image=~".+"}[1m]), "actor", "$1", "image", ".*/([^/:]+):[^/]+")
70
+ or
71
+ label_replace(rate(container_pressure_cpu_waiting_seconds_total{image="", id=~".*/fc-[^/]+\\.(scope|slice)"}[1m]), "actor", "$1", "id", ".*/(fc-[^/]+)\\.(?:scope|slice)")
72
+ )
73
+ # The limits-doctrine tripwire (§6, G5): weights never quotas, so this
74
+ # must read 0 on EVERY cgroup — any increase means someone added a quota.
75
+ - record: fc:actor:throttled_periods_delta
76
+ expr: >-
77
+ sum by (actor) (
78
+ label_replace(increase(container_cpu_cfs_throttled_periods_total{image=~".+"}[1m]), "actor", "$1", "image", ".*/([^/:]+):[^/]+")
79
+ or
80
+ label_replace(increase(container_cpu_cfs_throttled_periods_total{image="", id=~".*/fc-[^/]+\\.(scope|slice)"}[1m]), "actor", "$1", "id", ".*/(fc-[^/]+)\\.(?:scope|slice)")
81
+ )
82
+
83
+ - name: fc-pg
84
+ interval: 15s
85
+ rules:
86
+ # db_family collapse (§3 row 6 / P1c F1-awareness): per-run DBs fold to a
87
+ # bounded family — a per-run datname must never survive into fc:*.
88
+ - record: fc:pg:backends
89
+ expr: >-
90
+ sum by (pg_node, db_family) (
91
+ label_replace(
92
+ label_replace(
93
+ label_replace(
94
+ label_replace(pg_stat_database_numbackends{datname!~"template.*|postgres|^$"}, "db_family", "other", "datname", ".*"),
95
+ "db_family", "e2e", "datname", "e2e_.*"),
96
+ "db_family", "integration", "datname", "integration_.*"),
97
+ "db_family", "standing", "datname", "forgecart")
98
+ )
99
+ - record: fc:pg:idle_in_tx_age_max_seconds
100
+ expr: max(pg_idle_in_tx_age_max_seconds)
101
+ - record: fc:pg:idle_in_tx_count
102
+ expr: sum(pg_idle_in_tx_count)
103
+ - record: fc:pg:backends_total
104
+ expr: sum by (pg_node) (pg_stat_database_numbackends)
105
+ # Cluster or-branch (#973 rollout): CNPG's NATIVE exporter serves this
106
+ # family as cnpg_backends_total (its own metric, not our custom-queries
107
+ # file, so the scrape-time cnpg_-prefix strip cannot claim it without
108
+ # colliding with the pg_ exporter namespace). The cloud vmagent attaches
109
+ # {pg_node,source} at scrape; per-env the two arms are disjoint.
110
+ - record: fc:pg:citus_backends
111
+ expr: >-
112
+ sum by (pg_node, source) (pg_citus_backends_count or cnpg_backends_total)
113
+
114
+ - name: fc-mq
115
+ interval: 15s
116
+ rules:
117
+ # Aggregated `/metrics` basics only in v1 (POC §3.2): per-queue_class
118
+ # series exist ONLY on the detailed endpoint and land with P1c/P4 (F4).
119
+ # Dark until the Q12 root-compose event publishes :15692.
120
+ - record: fc:mq:ready_total
121
+ expr: rabbitmq_queue_messages_ready
122
+ - record: fc:mq:unacked_total
123
+ expr: rabbitmq_queue_messages_unacked
124
+ - record: fc:mq:connections
125
+ expr: rabbitmq_connections
126
+ - record: fc:mq:channels
127
+ expr: rabbitmq_channels
128
+ - record: fc:mq:consumers
129
+ expr: rabbitmq_consumers
130
+
131
+ - name: fc-sock
132
+ interval: 15s
133
+ rules:
134
+ - record: fc:sock:tcp_states
135
+ expr: node_tcp_connection_states
136
+ - record: fc:sock:sockets_used
137
+ expr: node_sockstat_sockets_used
138
+
139
+ - name: fc-dns
140
+ interval: 15s
141
+ rules:
142
+ # probe=system rides getaddrinfo (HTTP probe of the nip host); control
143
+ # asks 1.1.1.1 directly. system-fail + control-success = router
144
+ # rebind-protection signature (§3 row 17).
145
+ - record: fc:dns:probe_success
146
+ expr: >-
147
+ label_replace(probe_success{job="dns-system"}, "probe", "system", "job", ".*")
148
+ or
149
+ label_replace(probe_success{job="dns-control"}, "probe", "control", "job", ".*")
150
+ - record: fc:dns:probe_duration_seconds
151
+ expr: >-
152
+ label_replace(probe_duration_seconds{job="dns-system"}, "probe", "system", "job", ".*")
153
+ or
154
+ label_replace(probe_duration_seconds{job="dns-control"}, "probe", "control", "job", ".*")
155
+
156
+ - name: fc-canary
157
+ interval: 15s
158
+ rules:
159
+ # The victim canary (G2's instrument): /health = liveness floor,
160
+ # GraphQL = the load-bearing latency gate.
161
+ - record: fc:canary:health_success
162
+ expr: probe_success{job="canary-health"}
163
+ - record: fc:canary:health_duration_seconds
164
+ expr: probe_duration_seconds{job="canary-health"}
165
+ - record: fc:canary:graphql_success
166
+ expr: probe_success{job="canary-graphql"}
167
+ - record: fc:canary:graphql_duration_seconds
168
+ expr: probe_duration_seconds{job="canary-graphql"}
169
+
170
+ - name: fc-backing
171
+ interval: 15s
172
+ rules:
173
+ - record: fc:redis:connected_clients
174
+ expr: redis_connected_clients
175
+ - record: fc:redis:blocked_clients
176
+ expr: redis_blocked_clients
177
+ - record: fc:es:up
178
+ expr: up{job="elasticsearch"}
179
+ - record: fc:ch:up
180
+ expr: up{job="clickhouse"}
181
+
182
+ - name: fc-app
183
+ interval: 15s
184
+ rules:
185
+ # P1a (#968) — the package/flight runtime instruments, scraped off the
186
+ # per-service /metrics listeners (:9464-68; `service` comes from the
187
+ # scrape target labels). fc_* app series → fc:* dashboard contract.
188
+ - record: fc:tx:p50_seconds
189
+ expr: histogram_quantile(0.5, sum by (le, op_class) (rate(fc_tx_duration_seconds_bucket[1m])))
190
+ - record: fc:tx:p99_seconds
191
+ expr: histogram_quantile(0.99, sum by (le, op_class) (rate(fc_tx_duration_seconds_bucket[1m])))
192
+ # F9: the live budget gauge, per service — never a rules-file constant.
193
+ - record: fc:tx:budget_seconds
194
+ expr: max by (service) (fc_tx_budget_seconds)
195
+ - record: fc:tx:ring_min_seconds
196
+ expr: max by (service) (fc_tx_ring_min_seconds)
197
+ - record: fc:tx:ring_max_seconds
198
+ expr: max by (service) (fc_tx_ring_max_seconds)
199
+ # F2: the all-traffic distribution (op_class=query|mutation|tx_mutation).
200
+ - record: fc:gql:p50_seconds
201
+ expr: histogram_quantile(0.5, sum by (le, op_class) (rate(fc_gql_duration_seconds_bucket[1m])))
202
+ - record: fc:gql:p99_seconds
203
+ expr: histogram_quantile(0.99, sum by (le, op_class) (rate(fc_gql_duration_seconds_bucket[1m])))
204
+ - record: fc:gql:rate
205
+ expr: sum by (op_class) (rate(fc_gql_duration_seconds_count[1m]))
206
+ - record: fc:pool:size
207
+ expr: sum by (service, lane) (fc_pool_size)
208
+ - record: fc:pool:in_use
209
+ expr: sum by (service, lane) (fc_pool_in_use)
210
+ - record: fc:pool:idle
211
+ expr: sum by (service, lane) (fc_pool_idle)
212
+ - record: fc:pool:waiting
213
+ expr: sum by (service, lane) (fc_pool_waiting)
214
+ - record: fc:pool:acquire_wait_p99_seconds
215
+ expr: histogram_quantile(0.99, sum by (le, lane) (rate(fc_pool_acquire_wait_seconds_bucket[1m])))
216
+ - record: fc:heldtx:open
217
+ expr: sum by (service) (fc_heldtx_open)
218
+ - record: fc:heldtx:oldest_age_seconds
219
+ expr: max by (service) (fc_heldtx_oldest_age_seconds)
220
+ - record: fc:mq:confirm_p99_seconds
221
+ expr: histogram_quantile(0.99, sum by (le) (rate(fc_mq_confirm_duration_seconds_bucket[1m])))
222
+ # F4: the REAL effective prefetch — the HOL-utilisation divisor.
223
+ - record: fc:mq:prefetch_effective
224
+ expr: max by (service) (fc_mq_prefetch_effective)
225
+ # The register-trigger meter (debt #3 ≥1k, debt #2/Kafka ≥5-10k): the
226
+ # 1k/5k reference lines live on the dashboard panel.
227
+ - record: fc:ingest:events_per_second
228
+ expr: sum(rate(fc_mq_publish_total[1m]))
229
+ - record: fc:ingest:events_per_second_by_class
230
+ expr: sum by (queue_class) (rate(fc_mq_publish_total[1m]))
231
+ # Event-loop lag off prom-client's default monitorEventLoopDelay.
232
+ - record: fc:loop:lag_p99_seconds
233
+ expr: max by (service) (nodejs_eventloop_lag_p99_seconds)
234
+ # gRPC per target — the generic package/grpc seam (P1b #969, F3):
235
+ # channel readiness from the 5s state poll, in-flight from the
236
+ # per-attempt wrap (issue-without-settle = the park signature).
237
+ - record: fc:grpc:ready
238
+ expr: max by (service, target) (fc_grpc_ready)
239
+ - record: fc:grpc:inflight
240
+ expr: sum by (service, target) (fc_grpc_inflight)
241
+ - record: fc:grpc:transient_failure_rate
242
+ expr: sum by (service, target) (rate(fc_grpc_transient_failure_seconds_total[5m]))
243
+
244
+ - name: fc-pgbouncer
245
+ interval: 15s
246
+ rules:
247
+ # pgbouncer_exporter families with the db_family collapse (P1c #970).
248
+ # Dark locally until the Q12 event lands the stats console; live in CI
249
+ # (the per-run compose carries the committed STATS_USERS edit).
250
+ - record: fc:pgbouncer:cl_active
251
+ expr: >-
252
+ sum by (db_family) (
253
+ label_replace(
254
+ label_replace(
255
+ label_replace(
256
+ label_replace(pgbouncer_pools_client_active_connections, "db_family", "other", "database", ".*"),
257
+ "db_family", "e2e", "database", "e2e_.*"),
258
+ "db_family", "integration", "database", "integration_.*"),
259
+ "db_family", "standing", "database", "forgecart")
260
+ )
261
+ - record: fc:pgbouncer:cl_waiting
262
+ expr: >-
263
+ sum by (db_family) (
264
+ label_replace(
265
+ label_replace(
266
+ label_replace(
267
+ label_replace(pgbouncer_pools_client_waiting_connections, "db_family", "other", "database", ".*"),
268
+ "db_family", "e2e", "database", "e2e_.*"),
269
+ "db_family", "integration", "database", "integration_.*"),
270
+ "db_family", "standing", "database", "forgecart")
271
+ )
272
+ # sv_active carries the same cluster or-branch (cnpg_pgbouncer_pools_sv_active).
273
+ - record: fc:pgbouncer:sv_active
274
+ expr: >-
275
+ sum by (db_family) (
276
+ label_replace(
277
+ label_replace(
278
+ label_replace(
279
+ label_replace(
280
+ (pgbouncer_pools_server_active_connections or cnpg_pgbouncer_pools_sv_active),
281
+ "db_family", "other", "database", ".*"),
282
+ "db_family", "e2e", "database", "e2e_.*"),
283
+ "db_family", "integration", "database", "integration_.*"),
284
+ "db_family", "standing", "database", "forgecart")
285
+ )
286
+ - record: fc:pgbouncer:sv_idle
287
+ expr: >-
288
+ sum by (db_family) (
289
+ label_replace(
290
+ label_replace(
291
+ label_replace(
292
+ label_replace(pgbouncer_pools_server_idle_connections, "db_family", "other", "database", ".*"),
293
+ "db_family", "e2e", "database", "e2e_.*"),
294
+ "db_family", "integration", "database", "integration_.*"),
295
+ "db_family", "standing", "database", "forgecart")
296
+ )
297
+ # Cluster or-branch (#973): CNPG's managed pooler exports its pool stats
298
+ # under cnpg_pgbouncer_pools_* — same strip-collision as backends above.
299
+ # CNPG splits maxwait into whole seconds + a micros remainder (live-scrape
300
+ # verified 2026-08-21; there is NO *_maxwait_seconds family) — recompose
301
+ # for local-parity precision.
302
+ - record: fc:pgbouncer:maxwait_seconds
303
+ expr: >-
304
+ max(
305
+ pgbouncer_pools_client_maxwait_seconds
306
+ or (cnpg_pgbouncer_pools_maxwait + cnpg_pgbouncer_pools_maxwait_us / 1e6)
307
+ )
308
+ # F5 — the raise-together invariant panel's rule: BOTH sides summed,
309
+ # denominator SCOPED (standing locally; prod aggregates across the two
310
+ # pooler replicas + per-DB pools at P4). PromQL's default matching
311
+ # returns EMPTY if either side isn't aggregated, and on() without sum()
312
+ # silently overstates the share — this exact rule is the audit's F5.
313
+ - record: fc:pgbouncer:pinned_share
314
+ expr: >-
315
+ sum(fc:pool:in_use{lane="held-tx"})
316
+ /
317
+ sum(fc:pgbouncer:sv_active{db_family="standing"})
318
+
319
+ - name: fc-convoy
320
+ interval: 15s
321
+ rules:
322
+ # §4.2 — one NAMED, legible boolean per multiplexing layer. The same
323
+ # file drives the panels AND the prod alerts (P4): sustained-ness lives
324
+ # in the alert `for:`, the booleans stay instantaneous truth.
325
+ - record: fc:convoy:heldtx
326
+ expr: max(fc:pool:waiting{lane="held-tx"}) > bool 0
327
+ - record: fc:convoy:pgbouncer
328
+ expr: max(fc:pgbouncer:cl_waiting) > bool 0
329
+ # Citus fan-out pressure against the compose CITUS_MAX_SHARED_POOL_SIZE
330
+ # (320) — 80% is the convoy line; P4 parameterizes per env.
331
+ - record: fc:convoy:citus_fanout
332
+ expr: max(sum by (pg_node) (fc:pg:citus_backends{source="citus"})) > bool 256
333
+ # Park signature (F3): RPCs in flight into a channel that is not ready.
334
+ - record: fc:convoy:grpc_parked
335
+ expr: >-
336
+ max by (service, target) (
337
+ (fc:grpc:inflight > bool 0) * (fc:grpc:ready == bool 0)
338
+ )
339
+ # Prefetch head-of-line: unacked pinned at consumers × the REAL
340
+ # effective prefetch (F4's divisor, never a hardcoded ×10). The second
341
+ # factor requires live consumers — zero-consumer backlog is an outbox
342
+ # signal, not HOL. Empty until prefetch_effective exists (honest gap).
343
+ - record: fc:convoy:prefetch_hol
344
+ expr: >-
345
+ (
346
+ max(fc:mq:unacked_total)
347
+ >= bool
348
+ clamp_min(max(fc:mq:consumers) * max(fc:mq:prefetch_effective), 1)
349
+ )
350
+ * (max(fc:mq:consumers) > bool 0)
351
+
352
+ # ── P4 (#973): leak row + outbox + the F1 workspace family ─────────────────
353
+ - name: fc-leak
354
+ interval: 15s
355
+ rules:
356
+ # Row 8 (leak detection, prod-first and identical locally): CLOSE_WAIT
357
+ # by actor from cAdvisor's tcp keep-list — a held-open peer socket the
358
+ # app never reaped; a climbing per-actor count is the classic fd leak.
359
+ - record: fc:leak:close_wait
360
+ expr: >-
361
+ max by (actor) (
362
+ label_replace(container_network_tcp_usage_total{tcp_state="close_wait"},
363
+ "actor", "$1", "name", "(.+)")
364
+ )
365
+ # RSS slope per actor (bytes/s over 30m) — the leak view that catches
366
+ # slow growth long before any fence: > ~1 MB/min sustained is a leak
367
+ # candidate; pair with fc:host:mem_available_ratio for urgency.
368
+ - record: fc:leak:rss_slope_bytes_per_s
369
+ expr: deriv(fc:actor:rss_bytes[30m])
370
+ - name: fc-outbox
371
+ interval: 15s
372
+ rules:
373
+ # FcOutboxOldest's feed — from the SAME pg-custom-queries.yaml the prod
374
+ # CNPG ConfigMap renders (pg_outbox query, #973).
375
+ - record: fc:outbox:oldest_seconds
376
+ expr: max(pg_outbox_oldest_unsent_seconds)
377
+ - record: fc:outbox:backlog
378
+ expr: max(pg_outbox_backlog)
379
+ - name: fc-workspace
380
+ interval: 15s
381
+ rules:
382
+ # F1 (CRITICAL) — bounded workspace-pod FAMILY series: no channel-code
383
+ # label EVER (per-tenant labels are the one tenant-count-proportional
384
+ # axis). tier derives from NAMESPACE at scrape time (prod vmagent
385
+ # relabel: shop-workspaces-* → workspace-pod, runner ns → ci-runner);
386
+ # locally the pods nest inside the k3s container and these evaluate
387
+ # empty — the honest local state; per-pod truth lives in Plane B/replay.
388
+ # The tenant-count counter survives as the pods_count sum (debt #1's
389
+ # ~1k trigger line rides FcCardinalityBudget).
390
+ - record: fc:workspace:pods_count
391
+ expr: count by (tier) (container_memory_working_set_bytes{tier=~"workspace-pod|ci-runner"})
392
+ - record: fc:workspace:rss_bytes
393
+ expr: >-
394
+ sum by (tier) (container_memory_working_set_bytes{tier=~"workspace-pod|ci-runner"})
395
+ - record: fc:workspace:rss_bytes_max
396
+ expr: >-
397
+ max by (tier) (container_memory_working_set_bytes{tier=~"workspace-pod|ci-runner"})
398
+ - record: fc:workspace:cpu_cores
399
+ expr: >-
400
+ sum by (tier) (rate(container_cpu_usage_seconds_total{tier=~"workspace-pod|ci-runner"}[1m]))
401
+
402
+ - name: fc-self
403
+ interval: 15s
404
+ rules:
405
+ # Cardinality self-watch (C5): active series vs the 60k-local ceiling.
406
+ # The FcCardinalityBudget alert + the formal budget panel land in P1c;
407
+ # this rule is the placeholder feed (#967 acceptance, C5 partial).
408
+ - record: fc:self:active_series
409
+ expr: vm_cache_entries{type="storage/tsid"}
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@forgecart/flight-observability",
3
+ "version": "0.202608212205.0",
4
+ "private": false,
5
+ "description": "Flight Deck dashboards, fc: recording/alert rules, and the pg-custom-queries contract — the version-pinned sync artifact between shop-beta and forgecart/cloud (launch#46 P4).",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "publishConfig": {
10
+ "access": "public",
11
+ "registry": "https://registry.npmjs.org"
12
+ }
13
+ }