@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.
- package/dist/MANIFEST.md +9 -0
- package/dist/dashboards/flight-deck.json +2193 -0
- package/dist/dashboards/multiplexing.json +1771 -0
- package/dist/exporters/pg-custom-queries.yaml +64 -0
- package/dist/rules/fc-alerts.yml +188 -0
- package/dist/rules/fc-recording.yml +409 -0
- package/package.json +13 -0
|
@@ -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
|
+
}
|