Skip to content

Commit 992a3c6

Browse files
Surface run-summary projection missing-run age on dw system:operator-metrics
Renders the workflow-v2 run-summary projection-lag age pair on the operator-metrics CLI surface so HTTP-only operators can read "how long has the worst-case run been without a run-summary projection?" — the primary projection-lag age indicator on the run-summary path — directly from `dw system:operator-metrics`. `OperatorMetricsCommand::renderProjections()` is a new section that emits two rows under "Projection lag": Run-summary missing age: <ms> Oldest run-summary missing run at: <ISO-8601> The renderer omits the entire section when the snapshot predates the contract, so older alphas continue to render without a stray "Projection lag" header. Existing repair-section "Oldest missing-task age" rendering of `repair.{max_missing_run_age_ms,oldest_missing_run_started_at}` remains unchanged — the new section reports projection lag, not the repair-detector signal. Pins `projections.run_summaries.oldest_missing_run_started_at` and `projections.run_summaries.max_missing_run_age_ms` on `schemas/output/operator-metrics.schema.json`, and adds `test_operator_metrics_schema_pins_run_summary_missing_age_keys` plus `test_operator_metrics_command_omits_run_summary_missing_age_when_snapshot_predates_contract` to guard the schema declaration and the renderer fallback.
1 parent 48c2ff6 commit 992a3c6

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

schemas/output/operator-metrics.schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,20 @@
145145
"task_dispatch_mode": { "type": ["string", "null"] }
146146
},
147147
"additionalProperties": true
148+
},
149+
"projections": {
150+
"type": "object",
151+
"properties": {
152+
"run_summaries": {
153+
"type": "object",
154+
"properties": {
155+
"oldest_missing_run_started_at": { "type": ["string", "null"] },
156+
"max_missing_run_age_ms": { "type": ["integer", "null"] }
157+
},
158+
"additionalProperties": true
159+
}
160+
},
161+
"additionalProperties": true
148162
}
149163
},
150164
"additionalProperties": true

src/Commands/SystemCommand/OperatorMetricsCommand.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6969
$this->renderTasks($output, $this->sectionArray($metrics, 'tasks'));
7070
$this->renderBacklog($output, $this->sectionArray($metrics, 'backlog'));
7171
$this->renderRepair($output, $this->sectionArray($metrics, 'repair'));
72+
$this->renderProjections($output, $this->sectionArray($metrics, 'projections'));
7273
$this->renderWorkers($output, $this->sectionArray($metrics, 'workers'));
7374
$this->renderBackend($output, $this->sectionArray($metrics, 'backend'));
7475
$this->renderMatchingRole($output, $this->sectionArray($metrics, 'matching_role'));
@@ -221,6 +222,34 @@ private function renderRepair(OutputInterface $output, array $repair): void
221222
$output->writeln('');
222223
}
223224

225+
/**
226+
* @param array<string, mixed> $projections
227+
*/
228+
private function renderProjections(OutputInterface $output, array $projections): void
229+
{
230+
$runSummaries = is_array($projections['run_summaries'] ?? null) ? $projections['run_summaries'] : [];
231+
232+
if (! array_key_exists('max_missing_run_age_ms', $runSummaries)
233+
&& ! is_string($runSummaries['oldest_missing_run_started_at'] ?? null)) {
234+
return;
235+
}
236+
237+
$output->writeln('<info>Projection lag</info>');
238+
if (array_key_exists('max_missing_run_age_ms', $runSummaries)) {
239+
$output->writeln(sprintf(
240+
' Run-summary missing age: %d ms',
241+
(int) ($runSummaries['max_missing_run_age_ms'] ?? 0),
242+
));
243+
}
244+
if (is_string($runSummaries['oldest_missing_run_started_at'] ?? null)) {
245+
$output->writeln(sprintf(
246+
' Oldest run-summary missing run at: %s',
247+
$runSummaries['oldest_missing_run_started_at'],
248+
));
249+
}
250+
$output->writeln('');
251+
}
252+
224253
/**
225254
* @param array<string, mixed> $workers
226255
*/

tests/Commands/SystemCommandTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ public function test_operator_metrics_command_renders_rollout_safety_signals():
539539
self::assertStringContainsString('Oldest missing-task age: 125000 ms', $display);
540540
self::assertStringContainsString('Oldest missing run at: 2026-04-24T11:27:55Z', $display);
541541

542+
self::assertStringContainsString('Projection lag', $display);
543+
self::assertStringContainsString('Run-summary missing age: 330000 ms', $display);
544+
self::assertStringContainsString('Oldest run-summary missing run at: 2026-04-24T11:24:30Z', $display);
545+
542546
self::assertStringContainsString('Required compatibility: build-2026.04.24', $display);
543547
self::assertStringContainsString('Active workers: 2 (2 queue scopes, 1 supporting required)', $display);
544548
self::assertStringNotContainsString(
@@ -788,6 +792,40 @@ public function test_operator_metrics_command_omits_dispatch_failed_age_when_sna
788792
self::assertStringContainsString('dispatch failed 2', $display);
789793
}
790794

795+
public function test_operator_metrics_schema_pins_run_summary_missing_age_keys(): void
796+
{
797+
$schema = json_decode(
798+
(string) file_get_contents(__DIR__.'/../../schemas/output/operator-metrics.schema.json'),
799+
true,
800+
flags: JSON_THROW_ON_ERROR,
801+
);
802+
803+
$runSummaries = $schema['properties']['operator_metrics']['properties']
804+
['projections']['properties']['run_summaries']['properties'];
805+
806+
self::assertSame(['string', 'null'], $runSummaries['oldest_missing_run_started_at']['type']);
807+
self::assertSame(['integer', 'null'], $runSummaries['max_missing_run_age_ms']['type']);
808+
}
809+
810+
public function test_operator_metrics_command_omits_run_summary_missing_age_when_snapshot_predates_contract(): void
811+
{
812+
$payload = self::operatorMetricsPayload();
813+
unset($payload['operator_metrics']['projections']);
814+
815+
$command = new OperatorMetricsCommand();
816+
$command->setServerClient(new SystemFakeClient($payload));
817+
818+
$tester = new CommandTester($command);
819+
self::assertSame(Command::SUCCESS, $tester->execute([]));
820+
821+
$display = $tester->getDisplay();
822+
823+
self::assertStringNotContainsString('Projection lag', $display);
824+
self::assertStringNotContainsString('Run-summary missing age', $display);
825+
self::assertStringNotContainsString('Oldest run-summary missing run at', $display);
826+
self::assertStringContainsString('Oldest missing-task age: 125000 ms', $display);
827+
}
828+
791829
public function test_operator_metrics_command_omits_activities_block_when_payload_lacks_it(): void
792830
{
793831
$payload = self::operatorMetricsPayload();
@@ -959,6 +997,12 @@ private static function operatorMetricsPayload(): array
959997
'shape' => 'in_worker',
960998
'task_dispatch_mode' => 'queue',
961999
],
1000+
'projections' => [
1001+
'run_summaries' => [
1002+
'oldest_missing_run_started_at' => '2026-04-24T11:24:30Z',
1003+
'max_missing_run_age_ms' => 330000,
1004+
],
1005+
],
9621006
],
9631007
];
9641008
}

0 commit comments

Comments
 (0)