Complete local Prometheus mapping and record live acceptance blockers
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e6f1-443f-7783-9920-a16b2ffc467f
This commit is contained in:
parent
27df597a39
commit
b6d0f78f93
11 changed files with 501 additions and 5 deletions
|
|
@ -40,5 +40,7 @@ operator delivery proof.
|
||||||
and Grafana for railiance01, with telemetry.coulomb.social as the intended
|
and Grafana for railiance01, with telemetry.coulomb.social as the intended
|
||||||
Grafana hostname. This repo retains Q2 contracts and rule meaning. The local
|
Grafana hostname. This repo retains Q2 contracts and rule meaning. The local
|
||||||
receiver and activity candidates are reference tooling; do not enable them as
|
receiver and activity candidates are reference tooling; do not enable them as
|
||||||
a second production monitoring plane. Runtime deployment remains pending in
|
a second production monitoring plane. The package records private installation
|
||||||
the package foundation workplan.
|
and restore proof; delivery acceptance remains blocked in RTEL-WP-0002-T04.
|
||||||
|
The [Prometheus mapping](docs/prometheus-mapping.md) supplies a tested report
|
||||||
|
exporter and failure/absence rules for package integration.
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
| Kind | ID | Status | Lane | Source |
|
| Kind | ID | Status | Lane | Source |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| workplan | RTEL-WP-0002 | active | — | workplans/RTEL-WP-0002-signal-contract.md |
|
| workplan | RTEL-WP-0002 | blocked | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||||
| workplan | RTELE-WP-0001 | finished | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
| workplan | RTELE-WP-0001 | finished | — | workplans/RTELE-WP-0001-statehub-bootstrap.md |
|
||||||
| task | RTEL-WP-0002-T01 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
| task | RTEL-WP-0002-T01 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||||
| task | RTEL-WP-0002-T02 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
| task | RTEL-WP-0002-T02 | done | — | workplans/RTEL-WP-0002-signal-contract.md |
|
||||||
|
|
|
||||||
35
contracts/platform-assurance-2026-09-28.json
Normal file
35
contracts/platform-assurance-2026-09-28.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"schema": "railiance-telemetry.stream.v1",
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"recipient": "railiance-platform-operator",
|
||||||
|
"signals": [
|
||||||
|
"apps-pg.ready",
|
||||||
|
"apps-pg.backup",
|
||||||
|
"apps-pg.wal",
|
||||||
|
"apps-pg.restore",
|
||||||
|
"apps-pg.headroom",
|
||||||
|
"platform-pg.ready",
|
||||||
|
"platform-pg.backup",
|
||||||
|
"platform-pg.wal",
|
||||||
|
"platform-pg.restore",
|
||||||
|
"platform-pg.headroom",
|
||||||
|
"platform-pg-2.ready",
|
||||||
|
"platform-pg-2.backup",
|
||||||
|
"platform-pg-2.wal",
|
||||||
|
"platform-pg-2.restore",
|
||||||
|
"platform-pg-2.headroom",
|
||||||
|
"openbao.seal",
|
||||||
|
"openbao.snapshot",
|
||||||
|
"openbao.restore",
|
||||||
|
"offsite.upload",
|
||||||
|
"offsite.restore",
|
||||||
|
"eso.ready",
|
||||||
|
"eso.refresh",
|
||||||
|
"forgejo-db.restore",
|
||||||
|
"eso.token-renewal"
|
||||||
|
],
|
||||||
|
"max_event_age_seconds": 900,
|
||||||
|
"heartbeat_seconds": 900,
|
||||||
|
"retention_days": 30
|
||||||
|
}
|
||||||
56
docs/prometheus-mapping.md
Normal file
56
docs/prometheus-mapping.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Platform assurance Prometheus mapping
|
||||||
|
|
||||||
|
RTEL-WP-0002-T04 supplies `scripts/prometheus_export.py` and
|
||||||
|
`rules/platform-assurance.json` for integration by rapp-telemetry. These are local
|
||||||
|
artifacts; no listener, scraper, schedule or Alertmanager route is installed.
|
||||||
|
|
||||||
|
Use `contracts/platform-assurance-2026-09-28.json` with current platform reports.
|
||||||
|
It adds the producer-owned `eso.token-renewal` classification to the original 23
|
||||||
|
signals. The original contract stays unchanged for existing SQLite databases:
|
||||||
|
do not change their binding without an explicit migration. Export consumes the
|
||||||
|
evaluation report itself, not the evidence envelope's `observation`/`evaluation`
|
||||||
|
wrapper. Unknown fields, signals, states and scope are rejected by the adapter.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/prometheus_export.py \
|
||||||
|
--contract contracts/platform-assurance-2026-09-28.json \
|
||||||
|
/path/to/fresh-platform-report.json
|
||||||
|
promtool check rules rules/platform-assurance.json
|
||||||
|
promtool test rules tests/prometheus-rules.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The exporter prints Prometheus text to stdout only after validation; errors use
|
||||||
|
stderr and exit 2. The package must publish via a temporary file and atomic rename
|
||||||
|
only on success, or serve successful output through an admitted private exporter.
|
||||||
|
Never redirect directly over the currently scraped file. No textfile collector
|
||||||
|
is assumed to exist. The package must select and admit that transport and executor.
|
||||||
|
|
||||||
|
Each signal has five `railiance_assurance_state` gauge series, exactly one set to
|
||||||
|
1. Labels are bounded by the contract (`stream`, `producer`, `signal`, `state`).
|
||||||
|
No logs, credentials or free-text producer details are exported. The separate
|
||||||
|
`railiance_assurance_observed_timestamp_seconds` gauge holds original evaluation
|
||||||
|
time; re-export does not refresh it. Reports older than 900 seconds or in the
|
||||||
|
future are rejected. State and timestamp metric families must be scraped together
|
||||||
|
from one complete export for this stream.
|
||||||
|
|
||||||
|
`RailianceAssuranceUnhealthy` fires for each non-healthy classification.
|
||||||
|
`RailianceAssuranceEmissionAbsent` fires for never-seen/disappeared timestamp
|
||||||
|
series, observation age over 900 seconds, or future observation time. These rules
|
||||||
|
have no additional `for` delay. They retain source semantics and the proposed
|
||||||
|
15-minute transport budget. They do not reclassify backup/restore age.
|
||||||
|
|
||||||
|
Platform's token-renewal signal currently uses a 36-hour source age threshold.
|
||||||
|
It reaches the unhealthy rule with all other source classifications. The inbox
|
||||||
|
request for failed CronJobs or no success for 48 hours (platform RPF-WP-0046-T06)
|
||||||
|
is not closed by this mapping: exact Job-failure observation and actual delivery
|
||||||
|
still need owner acceptance. Do not silently replace S3's threshold in Q2.
|
||||||
|
|
||||||
|
The JSON rule file is valid Prometheus YAML input. Package integration must wrap
|
||||||
|
its groups in the selected PrometheusRule/release configuration and verify live
|
||||||
|
selectors, loading and routing. The warning/owner labels do not identify a
|
||||||
|
confirmed human recipient. Prometheus loss cannot be detected by its own rules;
|
||||||
|
an outside-node heartbeat receiver and acknowledged failure/absence drill remain
|
||||||
|
mandatory. The SQLite inbox and local rule tests cannot satisfy that gate.
|
||||||
|
|
||||||
|
Format and validation references: [Prometheus exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/)
|
||||||
|
and [native rule testing](https://prometheus.io/docs/prometheus/3.7/configuration/unit_testing_rules/).
|
||||||
|
|
@ -110,3 +110,8 @@ implementation. Do not activate its candidate activity definitions as a parallel
|
||||||
production monitoring plane. Q2 now needs a reviewed Prometheus exporter/rule
|
production monitoring plane. Q2 now needs a reviewed Prometheus exporter/rule
|
||||||
mapping preserving the existing state semantics, and actual Alertmanager delivery
|
mapping preserving the existing state semantics, and actual Alertmanager delivery
|
||||||
acceptance. Package runtime/admission is tracked by RAPP-TELEMETRY-WP-0001.
|
acceptance. Package runtime/admission is tracked by RAPP-TELEMETRY-WP-0001.
|
||||||
|
|
||||||
|
September 28: the local [Prometheus mapping](prometheus-mapping.md) now supplies
|
||||||
|
that exporter and tested failure/absence rules. It uses a separate dated contract
|
||||||
|
for the added platform token-renewal signal. Package integration and actual
|
||||||
|
delivery acceptance remain blocked under RTEL-WP-0002-T04.
|
||||||
|
|
|
||||||
38
history/2026-09-28-loose-end-review.md
Normal file
38
history/2026-09-28-loose-end-review.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Loose-end review — 2026-09-28
|
||||||
|
|
||||||
|
Reviewed both repository workplans: RTELE-WP-0001 is finished with all three
|
||||||
|
tasks done; RTEL-WP-0002 has T01–T03 done and T04 waiting. No proposed, ready or
|
||||||
|
other unfinished workplans exist in this checkout. No whole task can honestly
|
||||||
|
close without live acceptance, but T04's exporter/rule implementation is now done.
|
||||||
|
|
||||||
|
Added a strict stdout Prometheus exporter, a dated 24-signal contract including
|
||||||
|
`eso.token-renewal`, failure/absence rules, and native rule fixtures. Preserved
|
||||||
|
the original contract for existing SQLite bindings and producer evaluation time
|
||||||
|
for absence detection. Updated the README's stale package-deployment claim.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- `python3 -m unittest discover -s tests -v`: 19 passed.
|
||||||
|
- Prometheus 3.5.0 `promtool check rules rules/platform-assurance.json`: two valid rules.
|
||||||
|
- `promtool test rules tests/prometheus-rules.json`: seven scenarios passed
|
||||||
|
(never seen, fresh, exact budget, stopped producer, future clock, disappeared
|
||||||
|
scrape and failed token renewal).
|
||||||
|
- `promtool check metrics`: passed on exported platform September 27 evidence.
|
||||||
|
That compatibility check used the original evaluation time, not a claim that
|
||||||
|
yesterday's evidence is fresh today.
|
||||||
|
|
||||||
|
Reviewed platform source at `c3607ff`, including
|
||||||
|
`docs/evidence/2026-09-27-service-assurance.json` and the source contract, and
|
||||||
|
rapp-telemetry records at `6d2e9fe`. The package records private install/restore
|
||||||
|
as complete. Its T04 retains delivery, outside-node watchdog and recurring
|
||||||
|
backup admission. No live infrastructure was modified or notification sent.
|
||||||
|
|
||||||
|
The September 23 platform inbox request is acknowledged as read. Its existing
|
||||||
|
RPF-WP-0046-T06 retains exact failed-Job/48-hour delivery acceptance; the current
|
||||||
|
S3 renewal signal uses 36 hours and must not be silently redefined here.
|
||||||
|
|
||||||
|
RTEL-WP-0002 is now blocked; T04 remains wait for accepted package transport,
|
||||||
|
confirmed recipient/channel, actual acknowledged failure/absence, independent
|
||||||
|
watchdog and recurring backup ownership. Bernd Worsch supplies recipient and
|
||||||
|
admission decisions; rapp-telemetry/platform retain runtime/custody integration.
|
||||||
|
No tasks or workplans were created, and no unfinished workplan was closed.
|
||||||
25
rules/platform-assurance.json
Normal file
25
rules/platform-assurance.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "railiance-platform-assurance",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"alert": "RailianceAssuranceUnhealthy",
|
||||||
|
"expr": "railiance_assurance_state{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\",state!=\"healthy\"} == 1",
|
||||||
|
"labels": {
|
||||||
|
"severity": "warning",
|
||||||
|
"owner": "railiance-telemetry"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"expr": "absent(railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}) or (time() - railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"} > 900) or (railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"} > time())",
|
||||||
|
"labels": {
|
||||||
|
"severity": "warning",
|
||||||
|
"owner": "railiance-telemetry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
55
scripts/prometheus_export.py
Normal file
55
scripts/prometheus_export.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render a validated S3 report as Prometheus text; no listener or delivery."""
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from platform_event import translate
|
||||||
|
from receiver import STATES, contract_check, instant, read_json
|
||||||
|
|
||||||
|
|
||||||
|
def labels(values):
|
||||||
|
def escape(value):
|
||||||
|
return value.replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
|
||||||
|
return '{' + ','.join(f'{key}="{escape(value)}"' for key, value in values.items()) + '}'
|
||||||
|
|
||||||
|
|
||||||
|
def render(report, contract, now):
|
||||||
|
contract_check(contract)
|
||||||
|
event = translate(report, contract)
|
||||||
|
observed = instant(event['observed_at']).timestamp()
|
||||||
|
if not 0 <= now.timestamp() - observed <= contract['max_event_age_seconds']:
|
||||||
|
raise ValueError('stale or future report')
|
||||||
|
identity = dict(stream=contract['stream'], producer=contract['producer'])
|
||||||
|
lines = ['# HELP railiance_assurance_state Producer classification, one hot per signal.',
|
||||||
|
'# TYPE railiance_assurance_state gauge']
|
||||||
|
for signal, state in sorted(event['states'].items()):
|
||||||
|
for candidate in sorted(STATES):
|
||||||
|
key = labels(dict(identity, signal=signal, state=candidate))
|
||||||
|
lines.append(f'railiance_assurance_state{key} {int(state == candidate)}')
|
||||||
|
lines.extend([
|
||||||
|
'# HELP railiance_assurance_observed_timestamp_seconds Original producer evaluation time.',
|
||||||
|
'# TYPE railiance_assurance_observed_timestamp_seconds gauge',
|
||||||
|
f'railiance_assurance_observed_timestamp_seconds{labels(identity)} {observed}',
|
||||||
|
])
|
||||||
|
return '\n'.join(lines) + '\n'
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--contract', required=True, type=Path)
|
||||||
|
parser.add_argument('report', type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
output = render(read_json(args.report), read_json(args.contract), datetime.now(timezone.utc))
|
||||||
|
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
||||||
|
print(json.dumps({'status': 'rejected', 'error': 'invalid-or-expired-report'}), file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
sys.stdout.write(output)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
207
tests/prometheus-rules.json
Normal file
207
tests/prometheus-rules.json
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
{
|
||||||
|
"rule_files": [
|
||||||
|
"../rules/platform-assurance.json"
|
||||||
|
],
|
||||||
|
"evaluation_interval": "1m",
|
||||||
|
"tests": [
|
||||||
|
{
|
||||||
|
"name": "never seen",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": [
|
||||||
|
{
|
||||||
|
"exp_labels": {
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"owner": "railiance-telemetry",
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "healthy fresh",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "0+60x20"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "20m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "20m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "budget boundary",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "0x16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "15m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "15m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stopped producer still scraped",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "0x16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "16m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": [
|
||||||
|
{
|
||||||
|
"exp_labels": {
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"owner": "railiance-telemetry",
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "16m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "future clock",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "600"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": [
|
||||||
|
{
|
||||||
|
"exp_labels": {
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"owner": "railiance-telemetry",
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "scrape disappears",
|
||||||
|
"interval": "1m",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "0 stale"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "6m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": [
|
||||||
|
{
|
||||||
|
"exp_labels": {
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"owner": "railiance-telemetry",
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "6m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "failed renewal",
|
||||||
|
"input_series": [
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_observed_timestamp_seconds{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\"}",
|
||||||
|
"values": "0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"series": "railiance_assurance_state{stream=\"railiance-platform.service-assurance\",producer=\"railiance-platform\",signal=\"eso.token-renewal\",state=\"failed\"}",
|
||||||
|
"values": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"alert_rule_test": [
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceEmissionAbsent",
|
||||||
|
"exp_alerts": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"eval_time": "0m",
|
||||||
|
"alertname": "RailianceAssuranceUnhealthy",
|
||||||
|
"exp_alerts": [
|
||||||
|
{
|
||||||
|
"exp_labels": {
|
||||||
|
"stream": "railiance-platform.service-assurance",
|
||||||
|
"producer": "railiance-platform",
|
||||||
|
"owner": "railiance-telemetry",
|
||||||
|
"severity": "warning",
|
||||||
|
"signal": "eso.token-renewal",
|
||||||
|
"state": "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
57
tests/test_prometheus_export.py
Normal file
57
tests/test_prometheus_export.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / 'scripts'))
|
||||||
|
from prometheus_export import labels, render
|
||||||
|
|
||||||
|
|
||||||
|
class ExportTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.now = datetime(2026, 9, 28, tzinfo=timezone.utc)
|
||||||
|
self.contract = json.loads((ROOT / 'contracts/platform-assurance-2026-09-28.json').read_text())
|
||||||
|
self.report = dict(schema='railiance-platform.assurance-signal.v1',
|
||||||
|
cluster_uid='a553c742-0115-43d4-99a4-a5ca56fe0786',
|
||||||
|
evaluated_at=self.now.isoformat(),
|
||||||
|
signals={s: dict(state='healthy', owner='railiance-platform') for s in self.contract['signals']},
|
||||||
|
transport='unmonitored', guarantees='unsupported',
|
||||||
|
threshold_status='local-diagnostic-only', healthy=True)
|
||||||
|
|
||||||
|
def test_all_states_preserved_including_token_renewal(self):
|
||||||
|
for state in ('healthy', 'failed', 'missing', 'unavailable', 'stale'):
|
||||||
|
self.report['signals']['eso.token-renewal']['state'] = state
|
||||||
|
self.report['healthy'] = state == 'healthy'
|
||||||
|
output = render(self.report, self.contract, self.now)
|
||||||
|
samples = [line for line in output.splitlines() if line.startswith('railiance_assurance_state')]
|
||||||
|
self.assertEqual(len(samples), 120)
|
||||||
|
self.assertEqual(sum(line.endswith(' 1') for line in samples), 24)
|
||||||
|
self.assertIn(f'signal="eso.token-renewal",state="{state}"}} 1', output)
|
||||||
|
|
||||||
|
def test_retry_preserves_time_and_does_not_mutate_report(self):
|
||||||
|
before = copy.deepcopy(self.report)
|
||||||
|
self.assertEqual(render(self.report, self.contract, self.now),
|
||||||
|
render(self.report, self.contract, self.now + timedelta(seconds=900)))
|
||||||
|
self.assertEqual(self.report, before)
|
||||||
|
for delta in (-1, 901):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
render(self.report, self.contract, self.now + timedelta(seconds=delta))
|
||||||
|
|
||||||
|
def test_scope_drift_is_rejected(self):
|
||||||
|
self.report['signals']['unexpected'] = dict(state='healthy', owner='railiance-platform')
|
||||||
|
with self.assertRaises(ValueError): render(self.report, self.contract, self.now)
|
||||||
|
|
||||||
|
def test_label_escaping(self):
|
||||||
|
self.assertEqual(labels({'signal': 'a"b\\c\nd'}), '{signal="a\\"b\\\\c\\nd"}')
|
||||||
|
|
||||||
|
def test_cli_failure_has_no_metrics_stdout(self):
|
||||||
|
result = subprocess.run([sys.executable, str(ROOT / 'scripts/prometheus_export.py'),
|
||||||
|
'--contract', str(ROOT / 'contracts/platform-assurance-2026-09-28.json'),
|
||||||
|
str(ROOT / 'tests/nonexistent-report.json')], capture_output=True, text=True)
|
||||||
|
self.assertEqual(result.returncode, 2)
|
||||||
|
self.assertEqual(result.stdout, '')
|
||||||
|
self.assertIn('invalid-or-expired-report', result.stderr)
|
||||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
||||||
title: "Provide the Q2 receiving contract and prove signal delivery"
|
title: "Provide the Q2 receiving contract and prove signal delivery"
|
||||||
domain: financials
|
domain: financials
|
||||||
repo: railiance-telemetry
|
repo: railiance-telemetry
|
||||||
status: active
|
status: blocked
|
||||||
flavor: implementation
|
flavor: implementation
|
||||||
owner: codex
|
owner: codex
|
||||||
created: "2026-09-06"
|
created: "2026-09-06"
|
||||||
updated: "2026-09-06"
|
updated: "2026-09-28"
|
||||||
related:
|
related:
|
||||||
- RTELE-WP-0001
|
- RTELE-WP-0001
|
||||||
- RPF-WP-0036
|
- RPF-WP-0036
|
||||||
|
|
@ -96,3 +96,19 @@ package install, readiness and authenticated exposure. This T04 retains Q2's
|
||||||
exporter/rule mapping and actual delivery/absence acceptance. Existing SQLite
|
exporter/rule mapping and actual delivery/absence acceptance. Existing SQLite
|
||||||
runtime and disabled activities are reference work, not the production service.
|
runtime and disabled activities are reference work, not the production service.
|
||||||
No package activation or Q2 delivery acceptance is claimed by repo creation.
|
No package activation or Q2 delivery acceptance is claimed by repo creation.
|
||||||
|
|
||||||
|
September 28 loose-end review: completed the local Prometheus text exporter and
|
||||||
|
native-tested failure/absence rules under this task. A separate dated contract
|
||||||
|
includes platform's added `eso.token-renewal` signal without silently migrating
|
||||||
|
existing SQLite bindings. Source classifications and evaluation time survive
|
||||||
|
export; stale/future reports fail. Nineteen Python tests, seven native Prometheus
|
||||||
|
rule scenarios and metric lint pass. See `docs/prometheus-mapping.md` and
|
||||||
|
`history/2026-09-28-loose-end-review.md`.
|
||||||
|
|
||||||
|
Package T03 is already done (private installation and attended restore proof).
|
||||||
|
T04 remains `wait` and this workplan is now `blocked`: rapp-telemetry T04 still
|
||||||
|
owes accepted scrape/executor binding, a confirmed recipient/channel, actual
|
||||||
|
failure/absence acknowledgments, an outside-node watchdog and recurring backup
|
||||||
|
ownership. The founder, Bernd Worsch, supplies recipient/admission decisions;
|
||||||
|
rapp-telemetry and platform own runtime/custody integration. No additional task
|
||||||
|
or workplan was opened, and no live schedule or notification was enabled.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue